WebAuthn support for native applications. Support custom FIDO2 origin validation (#23156)
Closes #23155
This commit is contained in:
parent
6074cbf311
commit
31759f9c37
16 changed files with 145 additions and 20 deletions
|
@ -138,6 +138,7 @@ public class RealmRepresentation {
|
||||||
protected Integer webAuthnPolicyCreateTimeout;
|
protected Integer webAuthnPolicyCreateTimeout;
|
||||||
protected Boolean webAuthnPolicyAvoidSameAuthenticatorRegister;
|
protected Boolean webAuthnPolicyAvoidSameAuthenticatorRegister;
|
||||||
protected List<String> webAuthnPolicyAcceptableAaguids;
|
protected List<String> webAuthnPolicyAcceptableAaguids;
|
||||||
|
protected List<String> webAuthnPolicyExtraOrigins;
|
||||||
|
|
||||||
// WebAuthn passwordless properties below
|
// WebAuthn passwordless properties below
|
||||||
|
|
||||||
|
@ -151,6 +152,7 @@ public class RealmRepresentation {
|
||||||
protected Integer webAuthnPolicyPasswordlessCreateTimeout;
|
protected Integer webAuthnPolicyPasswordlessCreateTimeout;
|
||||||
protected Boolean webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister;
|
protected Boolean webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister;
|
||||||
protected List<String> webAuthnPolicyPasswordlessAcceptableAaguids;
|
protected List<String> webAuthnPolicyPasswordlessAcceptableAaguids;
|
||||||
|
protected List<String> webAuthnPolicyPasswordlessExtraOrigins;
|
||||||
|
|
||||||
// Client Policies/Profiles
|
// Client Policies/Profiles
|
||||||
|
|
||||||
|
@ -1127,6 +1129,14 @@ public class RealmRepresentation {
|
||||||
this.webAuthnPolicyAcceptableAaguids = webAuthnPolicyAcceptableAaguids;
|
this.webAuthnPolicyAcceptableAaguids = webAuthnPolicyAcceptableAaguids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getWebAuthnPolicyExtraOrigins(){
|
||||||
|
return webAuthnPolicyExtraOrigins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebAuthnPolicyExtraOrigins(List<String> extraOrigins) {
|
||||||
|
this.webAuthnPolicyExtraOrigins = extraOrigins;
|
||||||
|
}
|
||||||
|
|
||||||
// WebAuthn passwordless properties below
|
// WebAuthn passwordless properties below
|
||||||
|
|
||||||
|
|
||||||
|
@ -1210,6 +1220,14 @@ public class RealmRepresentation {
|
||||||
this.webAuthnPolicyPasswordlessAcceptableAaguids = webAuthnPolicyPasswordlessAcceptableAaguids;
|
this.webAuthnPolicyPasswordlessAcceptableAaguids = webAuthnPolicyPasswordlessAcceptableAaguids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getWebAuthnPolicyPasswordlessExtraOrigins(){
|
||||||
|
return webAuthnPolicyPasswordlessExtraOrigins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWebAuthnPolicyPasswordlessExtraOrigins(List<String> extraOrigins) {
|
||||||
|
this.webAuthnPolicyPasswordlessExtraOrigins = extraOrigins;
|
||||||
|
}
|
||||||
|
|
||||||
// Client Policies/Profiles
|
// Client Policies/Profiles
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
|
|
|
@ -2386,7 +2386,9 @@
|
||||||
"webAuthnPolicyCreateTimeoutHint": "Timeout needs to be between 0 seconds and 8 hours",
|
"webAuthnPolicyCreateTimeoutHint": "Timeout needs to be between 0 seconds and 8 hours",
|
||||||
"webAuthnPolicyAvoidSameAuthenticatorRegister": "Avoid same authenticator registration",
|
"webAuthnPolicyAvoidSameAuthenticatorRegister": "Avoid same authenticator registration",
|
||||||
"webAuthnPolicyAcceptableAaguids": "Acceptable AAGUIDs",
|
"webAuthnPolicyAcceptableAaguids": "Acceptable AAGUIDs",
|
||||||
|
"webAuthnPolicyExtraOrigins": "Extra Origins",
|
||||||
"addAaguids": "Add AAGUID",
|
"addAaguids": "Add AAGUID",
|
||||||
|
"addOrigins": "Add Origin",
|
||||||
"webAuthnUpdateSuccess": "Updated webauthn policies successfully",
|
"webAuthnUpdateSuccess": "Updated webauthn policies successfully",
|
||||||
"webAuthnUpdateError": "Could not update webauthn policies due to {{error}}",
|
"webAuthnUpdateError": "Could not update webauthn policies due to {{error}}",
|
||||||
"flowName": "Flow name",
|
"flowName": "Flow name",
|
||||||
|
@ -2496,6 +2498,7 @@
|
||||||
"webAuthnPolicyCreateTimeoutHelp": "Timeout value for creating user's public key credential in seconds. if set to 0, this timeout option is not adapted.",
|
"webAuthnPolicyCreateTimeoutHelp": "Timeout value for creating user's public key credential in seconds. if set to 0, this timeout option is not adapted.",
|
||||||
"webAuthnPolicyAvoidSameAuthenticatorRegisterHelp": "Avoid registering the authenticator that has already been registered.",
|
"webAuthnPolicyAvoidSameAuthenticatorRegisterHelp": "Avoid registering the authenticator that has already been registered.",
|
||||||
"webAuthnPolicyAcceptableAaguidsHelp": "The list of AAGUID of which an authenticator can be registered.",
|
"webAuthnPolicyAcceptableAaguidsHelp": "The list of AAGUID of which an authenticator can be registered.",
|
||||||
|
"webAuthnPolicyExtraOriginsHelp": "The list of extra origin for non-web application.",
|
||||||
"passwordPoliciesHelp": {
|
"passwordPoliciesHelp": {
|
||||||
"forceExpiredPasswordChange": "The number of days the password is valid before a new password is required.",
|
"forceExpiredPasswordChange": "The number of days the password is valid before a new password is required.",
|
||||||
"hashIterations": "The number of times a password is hashed before storage or verification. Default: 27,500.",
|
"hashIterations": "The number of times a password is hashed before storage or verification. Default: 27,500.",
|
||||||
|
|
|
@ -603,6 +603,8 @@
|
||||||
"webAuthnPolicyCreateTimeout": "タイムアウト",
|
"webAuthnPolicyCreateTimeout": "タイムアウト",
|
||||||
"webAuthnPolicyAvoidSameAuthenticatorRegister": "オーセンティケーターの重複登録回避",
|
"webAuthnPolicyAvoidSameAuthenticatorRegister": "オーセンティケーターの重複登録回避",
|
||||||
"webAuthnPolicyAcceptableAaguids": "許容可能なAAGUID",
|
"webAuthnPolicyAcceptableAaguids": "許容可能なAAGUID",
|
||||||
|
"webAuthnPolicyExtraOrigins": "エクストラオリジンズ",
|
||||||
|
"addOrigins": "オリジンを追加",
|
||||||
"default": "DEFAULT",
|
"default": "DEFAULT",
|
||||||
"flow": {
|
"flow": {
|
||||||
"browser": "ブラウザーフロー",
|
"browser": "ブラウザーフロー",
|
||||||
|
@ -640,6 +642,7 @@
|
||||||
"webAuthnPolicyCreateTimeoutHelp": "ユーザーの公開鍵クレデンシャルの作成に対するタイムアウト値(秒単位)。0に設定すると、このタイムアウト・オプションは適応されません。",
|
"webAuthnPolicyCreateTimeoutHelp": "ユーザーの公開鍵クレデンシャルの作成に対するタイムアウト値(秒単位)。0に設定すると、このタイムアウト・オプションは適応されません。",
|
||||||
"webAuthnPolicyAvoidSameAuthenticatorRegisterHelp": "すでに登録されているオーセンティケーターの登録を避けるかどうかを設定します。",
|
"webAuthnPolicyAvoidSameAuthenticatorRegisterHelp": "すでに登録されているオーセンティケーターの登録を避けるかどうかを設定します。",
|
||||||
"webAuthnPolicyAcceptableAaguidsHelp": "登録可能なオーセンティケーターのAAGUIDのリスト。",
|
"webAuthnPolicyAcceptableAaguidsHelp": "登録可能なオーセンティケーターのAAGUIDのリスト。",
|
||||||
|
"webAuthnPolicyExtraOriginsHelp": "非 Web アプリケーションの追加オリジンのリスト。",
|
||||||
"unlinkUsers": "ユーザーのリンクを解除する",
|
"unlinkUsers": "ユーザーのリンクを解除する",
|
||||||
"removeImported": "インポートを削除",
|
"removeImported": "インポートを削除",
|
||||||
"vendor": "ベンダー",
|
"vendor": "ベンダー",
|
||||||
|
|
|
@ -2342,7 +2342,9 @@
|
||||||
"webAuthnPolicyCreateTimeoutHint": "超时时间需要在 0 秒到 8 小时之间",
|
"webAuthnPolicyCreateTimeoutHint": "超时时间需要在 0 秒到 8 小时之间",
|
||||||
"webAuthnPolicyAvoidSameAuthenticatorRegister": "避免相同的身份验证器注册",
|
"webAuthnPolicyAvoidSameAuthenticatorRegister": "避免相同的身份验证器注册",
|
||||||
"webAuthnPolicyAcceptableAaguids": "可接受的 AAGUID",
|
"webAuthnPolicyAcceptableAaguids": "可接受的 AAGUID",
|
||||||
|
"webAuthnPolicyExtraOrigins": "额外的 Origin",
|
||||||
"addAaguids": "添加 AAGUID",
|
"addAaguids": "添加 AAGUID",
|
||||||
|
"addOrigins": "添加 Origin",
|
||||||
"webAuthnUpdateSuccess": "已成功更新 webauthn 策略",
|
"webAuthnUpdateSuccess": "已成功更新 webauthn 策略",
|
||||||
"webAuthnUpdateError": "由于{{error}},无法更新 webauthn 策略",
|
"webAuthnUpdateError": "由于{{error}},无法更新 webauthn 策略",
|
||||||
"flowName": "流程名称",
|
"flowName": "流程名称",
|
||||||
|
@ -2451,6 +2453,7 @@
|
||||||
"webAuthnPolicyCreateTimeoutHelp": "以秒为单位创建用户公钥凭证的超时值。如果设置为 0,则不适应此超时选项。",
|
"webAuthnPolicyCreateTimeoutHelp": "以秒为单位创建用户公钥凭证的超时值。如果设置为 0,则不适应此超时选项。",
|
||||||
"webAuthnPolicyAvoidSameAuthenticatorRegisterHelp": "避免注册已经被注册过的验证器。",
|
"webAuthnPolicyAvoidSameAuthenticatorRegisterHelp": "避免注册已经被注册过的验证器。",
|
||||||
"webAuthnPolicyAcceptableAaguidsHelp": "AAGUID 列表,其中可以注册验证者。",
|
"webAuthnPolicyAcceptableAaguidsHelp": "AAGUID 列表,其中可以注册验证者。",
|
||||||
|
"webAuthnPolicyExtraOriginsHelp": "额外的 Origin 列表,用于非网络应用程序。",
|
||||||
"密码策略": {
|
"密码策略": {
|
||||||
"forceExpiredPasswordChange": "在需要新密码之前,当前密码的有效天数。",
|
"forceExpiredPasswordChange": "在需要新密码之前,当前密码的有效天数。",
|
||||||
"hashIterations": "密码在存储或验证之前被散列的次数。默认值:27,500。",
|
"hashIterations": "密码在存储或验证之前被散列的次数。默认值:27,500。",
|
||||||
|
|
|
@ -349,6 +349,22 @@ export const WebauthnPolicy = ({
|
||||||
addButtonLabel="addAaguids"
|
addButtonLabel="addAaguids"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("webAuthnPolicyExtraOrigins")}
|
||||||
|
fieldId="webAuthnPolicyExtraOrigins"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText={t("webAuthnPolicyExtraOriginsHelp")}
|
||||||
|
fieldLabelId="webAuthnPolicyExtraOrigins"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MultiLineInput
|
||||||
|
name={`${namePrefix}ExtraOrigins`}
|
||||||
|
aria-label={t("webAuthnPolicyExtraOrigins")}
|
||||||
|
addButtonLabel="addOrigins"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
|
|
|
@ -961,6 +961,12 @@ public class RealmAdapter implements LegacyRealmModel, JpaModel<RealmEntity> {
|
||||||
acceptableAaguids = Arrays.asList(acceptableAaguidsString.split(","));
|
acceptableAaguids = Arrays.asList(acceptableAaguidsString.split(","));
|
||||||
policy.setAcceptableAaguids(acceptableAaguids);
|
policy.setAcceptableAaguids(acceptableAaguids);
|
||||||
|
|
||||||
|
String extraOriginsString = getAttribute(RealmAttributes.WEBAUTHN_POLICY_EXTRA_ORIGINS + attributePrefix);
|
||||||
|
List<String> extraOrigins = new ArrayList<>();
|
||||||
|
if (extraOriginsString != null && !extraOriginsString.isEmpty())
|
||||||
|
extraOrigins = Arrays.asList(extraOriginsString.split(","));
|
||||||
|
policy.setExtraOrigins(extraOrigins);
|
||||||
|
|
||||||
return policy;
|
return policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1004,6 +1010,14 @@ public class RealmAdapter implements LegacyRealmModel, JpaModel<RealmEntity> {
|
||||||
} else {
|
} else {
|
||||||
removeAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS + attributePrefix);
|
removeAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS + attributePrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> extraOrigins = policy.getExtraOrigins();
|
||||||
|
if (extraOrigins != null && !extraOrigins.isEmpty()) {
|
||||||
|
String extraOriginsString = String.join(",", extraOrigins);
|
||||||
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_EXTRA_ORIGINS + attributePrefix, extraOriginsString);
|
||||||
|
} else {
|
||||||
|
removeAttribute(RealmAttributes.WEBAUTHN_POLICY_EXTRA_ORIGINS + attributePrefix);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -50,6 +50,7 @@ public interface RealmAttributes {
|
||||||
String WEBAUTHN_POLICY_CREATE_TIMEOUT = "webAuthnPolicyCreateTimeout";
|
String WEBAUTHN_POLICY_CREATE_TIMEOUT = "webAuthnPolicyCreateTimeout";
|
||||||
String WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER = "webAuthnPolicyAvoidSameAuthenticatorRegister";
|
String WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER = "webAuthnPolicyAvoidSameAuthenticatorRegister";
|
||||||
String WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS = "webAuthnPolicyAcceptableAaguids";
|
String WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS = "webAuthnPolicyAcceptableAaguids";
|
||||||
|
String WEBAUTHN_POLICY_EXTRA_ORIGINS = "webAuthnPolicyExtraOrigins";
|
||||||
|
|
||||||
String ADMIN_EVENTS_EXPIRATION = "adminEventsExpiration";
|
String ADMIN_EVENTS_EXPIRATION = "adminEventsExpiration";
|
||||||
|
|
||||||
|
|
|
@ -1216,6 +1216,9 @@ public class LegacyExportImportManager implements ExportImportManager {
|
||||||
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
|
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
|
||||||
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
||||||
|
|
||||||
|
List<String> webAuthnPolicyExtraOrigins = rep.getWebAuthnPolicyExtraOrigins();
|
||||||
|
if (webAuthnPolicyExtraOrigins != null) webAuthnPolicy.setExtraOrigins(webAuthnPolicyExtraOrigins);
|
||||||
|
|
||||||
return webAuthnPolicy;
|
return webAuthnPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1268,6 +1271,9 @@ public class LegacyExportImportManager implements ExportImportManager {
|
||||||
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyPasswordlessAcceptableAaguids();
|
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyPasswordlessAcceptableAaguids();
|
||||||
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
||||||
|
|
||||||
|
List<String> webAuthnPolicyExtraOrigins = rep.getWebAuthnPolicyPasswordlessExtraOrigins();
|
||||||
|
if (webAuthnPolicyExtraOrigins != null) webAuthnPolicy.setExtraOrigins(webAuthnPolicyExtraOrigins);
|
||||||
|
|
||||||
return webAuthnPolicy;
|
return webAuthnPolicy;
|
||||||
}
|
}
|
||||||
public static Map<String, String> importAuthenticationFlows(KeycloakSession session, RealmModel newRealm, RealmRepresentation rep) {
|
public static Map<String, String> importAuthenticationFlows(KeycloakSession session, RealmModel newRealm, RealmRepresentation rep) {
|
||||||
|
|
|
@ -30,6 +30,8 @@ public class HotRodWebAuthnPolicyEntity extends AbstractHotRodEntity {
|
||||||
public List<String> acceptableAaguids;
|
public List<String> acceptableAaguids;
|
||||||
@ProtoField(number = 10)
|
@ProtoField(number = 10)
|
||||||
public List<String> signatureAlgorithms;
|
public List<String> signatureAlgorithms;
|
||||||
|
@ProtoField(number = 11)
|
||||||
|
public List<String> extraOrigins;
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
return HotRodWebAuthnPolicyEntityDelegate.entityEquals(this, o);
|
return HotRodWebAuthnPolicyEntityDelegate.entityEquals(this, o);
|
||||||
|
|
|
@ -1419,6 +1419,9 @@ public class MapExportImportManager implements ExportImportManager {
|
||||||
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
|
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
|
||||||
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
||||||
|
|
||||||
|
List<String> webAuthnPolicyExtraOrigins = rep.getWebAuthnPolicyExtraOrigins();
|
||||||
|
if (webAuthnPolicyExtraOrigins != null) webAuthnPolicy.setExtraOrigins(webAuthnPolicyExtraOrigins);
|
||||||
|
|
||||||
return webAuthnPolicy;
|
return webAuthnPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1471,6 +1474,9 @@ public class MapExportImportManager implements ExportImportManager {
|
||||||
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyPasswordlessAcceptableAaguids();
|
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyPasswordlessAcceptableAaguids();
|
||||||
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
||||||
|
|
||||||
|
List<String> webAuthnPolicyExtraOrigins = rep.getWebAuthnPolicyPasswordlessExtraOrigins();
|
||||||
|
if (webAuthnPolicyExtraOrigins != null) webAuthnPolicy.setExtraOrigins(webAuthnPolicyExtraOrigins);
|
||||||
|
|
||||||
return webAuthnPolicy;
|
return webAuthnPolicy;
|
||||||
}
|
}
|
||||||
public static Map<String, String> importAuthenticationFlows(KeycloakSession session, RealmModel newRealm, RealmRepresentation rep) {
|
public static Map<String, String> importAuthenticationFlows(KeycloakSession session, RealmModel newRealm, RealmRepresentation rep) {
|
||||||
|
|
|
@ -43,6 +43,7 @@ public interface MapWebAuthnPolicyEntity extends UpdatableEntity {
|
||||||
entity.setCreateTimeout(model.getCreateTimeout());
|
entity.setCreateTimeout(model.getCreateTimeout());
|
||||||
entity.setAvoidSameAuthenticatorRegister(model.isAvoidSameAuthenticatorRegister());
|
entity.setAvoidSameAuthenticatorRegister(model.isAvoidSameAuthenticatorRegister());
|
||||||
entity.setAcceptableAaguids(model.getAcceptableAaguids());
|
entity.setAcceptableAaguids(model.getAcceptableAaguids());
|
||||||
|
entity.setExtraOrigins(model.getExtraOrigins());
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +61,8 @@ public interface MapWebAuthnPolicyEntity extends UpdatableEntity {
|
||||||
model.setAvoidSameAuthenticatorRegister(entity.isAvoidSameAuthenticatorRegister());
|
model.setAvoidSameAuthenticatorRegister(entity.isAvoidSameAuthenticatorRegister());
|
||||||
List<String> acceptableAaguids = entity.getAcceptableAaguids();
|
List<String> acceptableAaguids = entity.getAcceptableAaguids();
|
||||||
model.setAcceptableAaguids(acceptableAaguids == null ? new LinkedList<>() : new LinkedList<>(acceptableAaguids));
|
model.setAcceptableAaguids(acceptableAaguids == null ? new LinkedList<>() : new LinkedList<>(acceptableAaguids));
|
||||||
|
List<String> extraOrigins = entity.getExtraOrigins();
|
||||||
|
model.setExtraOrigins(extraOrigins == null ? new LinkedList<>() : new LinkedList<>(extraOrigins));
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +78,7 @@ public interface MapWebAuthnPolicyEntity extends UpdatableEntity {
|
||||||
entity.setCreateTimeout(0);
|
entity.setCreateTimeout(0);
|
||||||
entity.setAvoidSameAuthenticatorRegister(false);
|
entity.setAvoidSameAuthenticatorRegister(false);
|
||||||
entity.setAcceptableAaguids(new LinkedList<>());
|
entity.setAcceptableAaguids(new LinkedList<>());
|
||||||
|
entity.setExtraOrigins(new LinkedList<>());
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,4 +111,7 @@ public interface MapWebAuthnPolicyEntity extends UpdatableEntity {
|
||||||
|
|
||||||
List<String> getAcceptableAaguids();
|
List<String> getAcceptableAaguids();
|
||||||
void setAcceptableAaguids(List<String> acceptableAaguids);
|
void setAcceptableAaguids(List<String> acceptableAaguids);
|
||||||
|
|
||||||
|
List<String> getExtraOrigins();
|
||||||
|
void setExtraOrigins(List<String> extraOrigins);
|
||||||
}
|
}
|
||||||
|
|
|
@ -489,6 +489,7 @@ public class ModelToRepresentation {
|
||||||
rep.setWebAuthnPolicyCreateTimeout(webAuthnPolicy.getCreateTimeout());
|
rep.setWebAuthnPolicyCreateTimeout(webAuthnPolicy.getCreateTimeout());
|
||||||
rep.setWebAuthnPolicyAvoidSameAuthenticatorRegister(webAuthnPolicy.isAvoidSameAuthenticatorRegister());
|
rep.setWebAuthnPolicyAvoidSameAuthenticatorRegister(webAuthnPolicy.isAvoidSameAuthenticatorRegister());
|
||||||
rep.setWebAuthnPolicyAcceptableAaguids(webAuthnPolicy.getAcceptableAaguids());
|
rep.setWebAuthnPolicyAcceptableAaguids(webAuthnPolicy.getAcceptableAaguids());
|
||||||
|
rep.setWebAuthnPolicyExtraOrigins(webAuthnPolicy.getExtraOrigins());
|
||||||
|
|
||||||
webAuthnPolicy = realm.getWebAuthnPolicyPasswordless();
|
webAuthnPolicy = realm.getWebAuthnPolicyPasswordless();
|
||||||
rep.setWebAuthnPolicyPasswordlessRpEntityName(webAuthnPolicy.getRpEntityName());
|
rep.setWebAuthnPolicyPasswordlessRpEntityName(webAuthnPolicy.getRpEntityName());
|
||||||
|
@ -501,6 +502,7 @@ public class ModelToRepresentation {
|
||||||
rep.setWebAuthnPolicyPasswordlessCreateTimeout(webAuthnPolicy.getCreateTimeout());
|
rep.setWebAuthnPolicyPasswordlessCreateTimeout(webAuthnPolicy.getCreateTimeout());
|
||||||
rep.setWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister(webAuthnPolicy.isAvoidSameAuthenticatorRegister());
|
rep.setWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister(webAuthnPolicy.isAvoidSameAuthenticatorRegister());
|
||||||
rep.setWebAuthnPolicyPasswordlessAcceptableAaguids(webAuthnPolicy.getAcceptableAaguids());
|
rep.setWebAuthnPolicyPasswordlessAcceptableAaguids(webAuthnPolicy.getAcceptableAaguids());
|
||||||
|
rep.setWebAuthnPolicyPasswordlessExtraOrigins(webAuthnPolicy.getExtraOrigins());
|
||||||
|
|
||||||
CibaConfig cibaPolicy = realm.getCibaPolicy();
|
CibaConfig cibaPolicy = realm.getCibaPolicy();
|
||||||
Map<String, String> attrMap = Optional.ofNullable(rep.getAttributes()).orElse(new HashMap<>());
|
Map<String, String> attrMap = Optional.ofNullable(rep.getAttributes()).orElse(new HashMap<>());
|
||||||
|
|
|
@ -40,6 +40,7 @@ public class WebAuthnPolicy implements Serializable {
|
||||||
protected int createTimeout = 0; // not specified as option
|
protected int createTimeout = 0; // not specified as option
|
||||||
protected boolean avoidSameAuthenticatorRegister = false;
|
protected boolean avoidSameAuthenticatorRegister = false;
|
||||||
protected List<String> acceptableAaguids;
|
protected List<String> acceptableAaguids;
|
||||||
|
protected List<String> extraOrigins;
|
||||||
|
|
||||||
public WebAuthnPolicy() {
|
public WebAuthnPolicy() {
|
||||||
}
|
}
|
||||||
|
@ -130,4 +131,12 @@ public class WebAuthnPolicy implements Serializable {
|
||||||
public void setAcceptableAaguids(List<String> acceptableAaguids) {
|
public void setAcceptableAaguids(List<String> acceptableAaguids) {
|
||||||
this.acceptableAaguids = acceptableAaguids;
|
this.acceptableAaguids = acceptableAaguids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getExtraOrigins(){
|
||||||
|
return extraOrigins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExtraOrigins(List<String> extraOrigins) {
|
||||||
|
this.extraOrigins = extraOrigins;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,15 +16,24 @@
|
||||||
|
|
||||||
package org.keycloak.credential;
|
package org.keycloak.credential;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import com.webauthn4j.WebAuthnAuthenticationManager;
|
import com.webauthn4j.WebAuthnAuthenticationManager;
|
||||||
|
import com.webauthn4j.authenticator.Authenticator;
|
||||||
|
import com.webauthn4j.authenticator.AuthenticatorImpl;
|
||||||
import com.webauthn4j.converter.util.ObjectConverter;
|
import com.webauthn4j.converter.util.ObjectConverter;
|
||||||
|
import com.webauthn4j.data.AuthenticationData;
|
||||||
|
import com.webauthn4j.data.AuthenticationParameters;
|
||||||
import com.webauthn4j.data.AuthenticatorTransport;
|
import com.webauthn4j.data.AuthenticatorTransport;
|
||||||
|
import com.webauthn4j.data.attestation.authenticator.AAGUID;
|
||||||
|
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
|
||||||
|
import com.webauthn4j.data.attestation.authenticator.COSEKey;
|
||||||
|
import com.webauthn4j.data.client.CollectedClientData;
|
||||||
|
import com.webauthn4j.data.client.Origin;
|
||||||
|
import com.webauthn4j.server.ServerProperty;
|
||||||
|
import com.webauthn4j.util.AssertUtil;
|
||||||
|
import com.webauthn4j.util.exception.WebAuthnException;
|
||||||
|
import com.webauthn4j.validator.OriginValidatorImpl;
|
||||||
|
import com.webauthn4j.validator.exception.BadOriginException;
|
||||||
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||||
import org.keycloak.common.util.Base64;
|
import org.keycloak.common.util.Base64;
|
||||||
|
@ -32,18 +41,16 @@ import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.WebAuthnPolicy;
|
||||||
import com.webauthn4j.authenticator.Authenticator;
|
|
||||||
import com.webauthn4j.authenticator.AuthenticatorImpl;
|
|
||||||
import com.webauthn4j.data.AuthenticationData;
|
|
||||||
import com.webauthn4j.data.AuthenticationParameters;
|
|
||||||
import com.webauthn4j.data.attestation.authenticator.AAGUID;
|
|
||||||
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
|
|
||||||
import com.webauthn4j.data.attestation.authenticator.COSEKey;
|
|
||||||
import com.webauthn4j.util.exception.WebAuthnException;
|
|
||||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||||
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
|
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Credential provider for WebAuthn 2-factor credential of the user
|
* Credential provider for WebAuthn 2-factor credential of the user
|
||||||
*/
|
*/
|
||||||
|
@ -180,7 +187,7 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
||||||
WebAuthnCredentialModelInput context = WebAuthnCredentialModelInput.class.cast(input);
|
WebAuthnCredentialModelInput context = WebAuthnCredentialModelInput.class.cast(input);
|
||||||
List<WebAuthnCredentialModelInput> auths = getWebAuthnCredentialModelList(realm, user);
|
List<WebAuthnCredentialModelInput> auths = getWebAuthnCredentialModelList(realm, user);
|
||||||
|
|
||||||
WebAuthnAuthenticationManager webAuthnAuthenticationManager = new WebAuthnAuthenticationManager();
|
WebAuthnAuthenticationManager webAuthnAuthenticationManager = getWebAuthnAuthenticationManager();
|
||||||
AuthenticationData authenticationData = null;
|
AuthenticationData authenticationData = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -233,6 +240,31 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected WebAuthnAuthenticationManager getWebAuthnAuthenticationManager() {
|
||||||
|
WebAuthnPolicy policy = getWebAuthnPolicy();
|
||||||
|
Set<Origin> origins = policy.getExtraOrigins().stream()
|
||||||
|
.map(Origin::new)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
WebAuthnAuthenticationManager webAuthnAuthenticationManager = new WebAuthnAuthenticationManager();
|
||||||
|
webAuthnAuthenticationManager.getAuthenticationDataValidator().setOriginValidator(new OriginValidatorImpl(){
|
||||||
|
@Override
|
||||||
|
protected void validate(@NonNull CollectedClientData collectedClientData,
|
||||||
|
@NonNull ServerProperty serverProperty) {
|
||||||
|
AssertUtil.notNull(collectedClientData, "collectedClientData must not be null");
|
||||||
|
AssertUtil.notNull(serverProperty, "serverProperty must not be null");
|
||||||
|
final Origin clientOrigin = collectedClientData.getOrigin();
|
||||||
|
if (serverProperty.getOrigins().contains(clientOrigin)) return;
|
||||||
|
// https://github.com/w3c/webauthn/issues/1297
|
||||||
|
if (origins.contains(clientOrigin)) return;
|
||||||
|
throw new BadOriginException("The collectedClientData '" + clientOrigin + "' origin doesn't match any of the preconfigured origins.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return webAuthnAuthenticationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected WebAuthnPolicy getWebAuthnPolicy() {
|
||||||
|
return session.getContext().getRealm().getWebAuthnPolicy();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getType() {
|
public String getType() {
|
||||||
|
|
|
@ -16,12 +16,9 @@
|
||||||
|
|
||||||
package org.keycloak.credential;
|
package org.keycloak.credential;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import com.webauthn4j.converter.util.ObjectConverter;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
import com.webauthn4j.converter.util.ObjectConverter;
|
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
|
||||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||||
|
|
||||||
public class WebAuthnCredentialProviderFactory implements CredentialProviderFactory<WebAuthnCredentialProvider>, EnvironmentDependentProviderFactory {
|
public class WebAuthnCredentialProviderFactory implements CredentialProviderFactory<WebAuthnCredentialProvider>, EnvironmentDependentProviderFactory {
|
||||||
|
|
|
@ -21,6 +21,7 @@ package org.keycloak.credential;
|
||||||
import com.webauthn4j.converter.util.ObjectConverter;
|
import com.webauthn4j.converter.util.ObjectConverter;
|
||||||
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
|
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.WebAuthnPolicy;
|
||||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,4 +52,9 @@ public class WebAuthnPasswordlessCredentialProvider extends WebAuthnCredentialPr
|
||||||
.removeable(true)
|
.removeable(true)
|
||||||
.build(getKeycloakSession());
|
.build(getKeycloakSession());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected WebAuthnPolicy getWebAuthnPolicy() {
|
||||||
|
return getKeycloakSession().getContext().getRealm().getWebAuthnPolicyPasswordless();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue