diff --git a/openam-authentication/openam-auth-webauthn/pom.xml b/openam-authentication/openam-auth-webauthn/pom.xml index 1a559e7ab5..d74ad155f0 100755 --- a/openam-authentication/openam-auth-webauthn/pom.xml +++ b/openam-authentication/openam-auth-webauthn/pom.xml @@ -12,7 +12,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2019 3A-Systems LLC + * Copyright 2024 3A-Systems LLC --> 4.0.0 @@ -24,7 +24,7 @@ 14.8.5-SNAPSHOT - 0.9.5.RELEASE + 0.21.9.RELEASE diff --git a/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnAuthentication.java b/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnAuthentication.java index 70b61980d1..99f98f31bd 100644 --- a/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnAuthentication.java +++ b/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnAuthentication.java @@ -11,45 +11,43 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyrighted [year] [name of copyright owner]". * - * Copyright 2019 3A-Systems LLC. All rights reserved. + * Copyright 2024 3A-Systems LLC. All rights reserved. */ package org.openidentityplatform.openam.authentication.modules.webauthn; -import java.security.Principal; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import javax.security.auth.Subject; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.PasswordCallback; -import javax.security.auth.callback.TextOutputCallback; -import javax.security.auth.login.LoginException; - -import org.apache.commons.lang.SerializationUtils; -import org.forgerock.openam.authentication.modules.common.mapping.AccountProvider; -import org.forgerock.openam.authentication.modules.common.mapping.DefaultAccountProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.iplanet.sso.SSOException; +import com.sun.identity.authentication.service.AuthD; +import com.sun.identity.authentication.service.AuthException; import com.sun.identity.authentication.spi.AMLoginModule; import com.sun.identity.authentication.spi.AuthLoginException; import com.sun.identity.authentication.spi.InvalidPasswordException; import com.sun.identity.authentication.util.ISAuthConstants; import com.sun.identity.idm.AMIdentity; import com.sun.identity.idm.IdRepoException; +import com.sun.identity.idm.IdType; import com.sun.identity.shared.datastruct.CollectionHelper; +import com.sun.identity.shared.debug.Debug; +import com.sun.identity.sm.DNMapper; import com.webauthn4j.authenticator.Authenticator; import com.webauthn4j.data.PublicKeyCredentialRequestOptions; import com.webauthn4j.data.attestation.authenticator.AuthenticatorData; +import org.apache.commons.lang.SerializationUtils; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.TextOutputCallback; +import javax.security.auth.login.LoginException; +import java.security.Principal; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; /** * @@ -58,7 +56,11 @@ */ public class WebAuthnAuthentication extends AMLoginModule { - final static Logger logger = LoggerFactory.getLogger(WebAuthnAuthentication.class); + final Debug debug; + + public WebAuthnAuthentication() { + debug = Debug.getInstance("amWebAuthnAuthentication"); + } final static ObjectMapper mapper = new ObjectMapper(); static { @@ -66,17 +68,8 @@ public class WebAuthnAuthentication extends AMLoginModule { } private final static int LOGIN_REQUEST_CREDENTIALS_STATE = 2; - private static final int CHALLENGE_ID_CB_INDEX = 0; - - private static final int CHALLENGE_AUTH_DATA_CB_INDEX = 1; - - private static final int CHALLENGE_CLIENT_DATA_CB_INDEX = 2; - - private static final int CHALLENGE_SIGNATURE_CB_INDEX = 3; - - private static final int CHALLENGE_USER_HANDLE_CB_INDEX = 4; - - private static final int CREDENTIAL_REQUEST_CB_INDEX = 5; + private static final int CREDENTIALS_CB_INDEX = 0; + private static final int CREDENTIAL_REQUEST_CB_INDEX = 1; private WebAuthnAuthenticationProcessor webAuthnAuthenticationProcessor = null; @@ -87,13 +80,10 @@ public class WebAuthnAuthentication extends AMLoginModule { private String userAttribute = null; private long timeout; - - private AccountProvider accountProvider = new DefaultAccountProvider(); - private Set authenticators; - private int authLevel = 0; - + + @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public void init(Subject subject, Map sharedState, Map options) { @@ -109,42 +99,29 @@ public void init(Subject subject, Map sharedState, Map options) { this.authLevel = Integer.parseInt(CollectionHelper.getMapAttr(options, WebAuthnAuthentication.class.getName().concat(".authlevel"), "0")); + + webAuthnAuthenticationProcessor = new WebAuthnAuthenticationProcessor( + getSessionId(), this.timeout); } @Override public int process(Callback[] callbacks, int state) throws LoginException { - if(webAuthnAuthenticationProcessor == null) { - webAuthnAuthenticationProcessor = new WebAuthnAuthenticationProcessor( - getSessionId(), this.timeout); - } - - + + try { switch (state) { case ISAuthConstants.LOGIN_START: - if(callbacks == null) { //no callback init authentication - if(username == null && sharedState.containsKey(getUserKey())) { - username = (String)sharedState.get(getUserKey()); - return requestCredentials(); - } else { - return ISAuthConstants.LOGIN_START; - } - } - else { - //getting username - username = ((NameCallback)callbacks[0]).getName(); - return requestCredentials(); - } + return requestCredentials(); case LOGIN_REQUEST_CREDENTIALS_STATE: return processCredentials(callbacks); default: break; } } catch(AuthLoginException e) { - logger.error("process: AuthLoginException {}", e.toString()); + debug.error("process: AuthLoginException {0}", e.toString()); throw e; } catch(Exception e) { - logger.error("process: Exception {}", e.toString()); + debug.error("process: Exception {0}", e.toString()); throw new AuthLoginException(e); } @@ -152,10 +129,9 @@ public int process(Callback[] callbacks, int state) throws LoginException { } public int requestCredentials() throws AuthLoginException, JsonProcessingException { - - authenticators = loadAuthenticators(); - PublicKeyCredentialRequestOptions credentialCreationOptions = - webAuthnAuthenticationProcessor.requestCredentials(username, getHttpServletRequest(), authenticators); + + PublicKeyCredentialRequestOptions credentialCreationOptions = + webAuthnAuthenticationProcessor.requestCredentials(getHttpServletRequest(), Collections.emptySet()); String credentialCreationOptionsString = mapper.writeValueAsString(credentialCreationOptions); TextOutputCallback credentialCreationOptionsCallback = new TextOutputCallback(TextOutputCallback.INFORMATION, credentialCreationOptionsString); @@ -165,22 +141,43 @@ public int requestCredentials() throws AuthLoginException, JsonProcessingExcepti } private int processCredentials(Callback[] callbacks) throws AuthLoginException { + + final String credentialsStr = new String(((PasswordCallback) callbacks[CREDENTIALS_CB_INDEX]).getPassword()); + final Map credentials; + try { + credentials = mapper.readValue(credentialsStr, new TypeReference>() {}); + } catch (Exception e) { + debug.error("invalid credentials data: " + credentialsStr, e); + throw new AuthLoginException(e); + } + + final String assertionId = credentials.get("assertionId"); + final String authenticatorDataStr = credentials.get("authenticatorData"); + final String clientDataJSONStr = credentials.get("clientDataJSON"); + final String signatureStr = credentials.get("signature"); + final String userHandleStr = credentials.get("userHandle"); + + String realm = DNMapper.orgNameToRealmName(getRequestOrg()); + byte[] userHandle = Base64Utils.decodeFromUrlSafeString(userHandleStr); + String userId = new String(userHandle); + + final AMIdentity identity; + try { + identity = AuthD.getAuth().getIdentity(IdType.USER, userId, realm); + } catch (AuthException e) { + throw new AuthLoginException(e); + } + final Set authenticators = loadAuthenticators(identity); - String id = new String(((PasswordCallback) callbacks[CHALLENGE_ID_CB_INDEX]).getPassword()); - String authenticatorDataStr = new String(((PasswordCallback) callbacks[CHALLENGE_AUTH_DATA_CB_INDEX]).getPassword()); - String clientDataJSONStr = new String(((PasswordCallback) callbacks[CHALLENGE_CLIENT_DATA_CB_INDEX]).getPassword()); - String signatureStr = new String(((PasswordCallback) callbacks[CHALLENGE_SIGNATURE_CB_INDEX]).getPassword()); - String userHandleStr = new String(((PasswordCallback) callbacks[CHALLENGE_USER_HANDLE_CB_INDEX]).getPassword()); - - - AuthenticatorData authenticatorData = webAuthnAuthenticationProcessor.processCredentials(getHttpServletRequest(), id, authenticatorDataStr, - clientDataJSONStr, signatureStr, userHandleStr, authenticators); + AuthenticatorData authenticatorData = webAuthnAuthenticationProcessor.processCredentials( + getHttpServletRequest(), assertionId, authenticatorDataStr, + clientDataJSONStr, signatureStr, userHandle, authenticators); if(authenticatorData == null) { - logger.warn("processCredentials: authenticator data with id {} not found for identity: {}", id, username); + debug.warning("processCredentials: authenticator data with id {0} not found for identity: {1}", assertionId, username); throw new InvalidPasswordException("authenticator not found"); } - + this.username = userId; setAuthLevel(this.authLevel); return ISAuthConstants.LOGIN_SUCCEED; @@ -194,21 +191,18 @@ public Principal getPrincipal() { } @SuppressWarnings("unchecked") - protected Set loadAuthenticators() throws AuthLoginException { + protected Set loadAuthenticators(AMIdentity identity) throws AuthLoginException { Set authenticators = new HashSet<>(); - - Map> attributes = new HashMap<>(); - attributes.put("uid", Collections.singleton(username)); - AMIdentity user = accountProvider.searchUser(getAMIdentityRepository(getRequestOrg()), attributes); + try { - Set authenticatorsMarshalled = user.getAttribute(userAttribute); + Set authenticatorsMarshalled = identity.getAttribute(userAttribute); for(String authenticatorMarshalled: authenticatorsMarshalled) { byte[] bytesDecoded = Base64Utils.decodeFromUrlSafeString(authenticatorMarshalled); Authenticator decoded = (Authenticator)SerializationUtils.deserialize(bytesDecoded); authenticators.add(decoded); } } catch (SSOException | IdRepoException e) { - logger.error("loadAuthenticators: error getting authenticators from user : {}", e); + debug.error("loadAuthenticators: error getting authenticators from user : {0}", e.toString()); throw new AuthLoginException(e); } return authenticators; diff --git a/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnAuthenticationProcessor.java b/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnAuthenticationProcessor.java index e1acd87495..76c389a334 100644 --- a/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnAuthenticationProcessor.java +++ b/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnAuthenticationProcessor.java @@ -11,48 +11,52 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyrighted [year] [name of copyright owner]". * - * Copyright 2019 3A-Systems LLC. All rights reserved. + * Copyright 2024 3A-Systems LLC. All rights reserved. */ package org.openidentityplatform.openam.authentication.modules.webauthn; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -import javax.servlet.http.HttpServletRequest; - -import org.apache.commons.lang.ArrayUtils; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.sun.identity.authentication.spi.AuthLoginException; +import com.webauthn4j.WebAuthnManager; import com.webauthn4j.authenticator.Authenticator; +import com.webauthn4j.converter.exception.DataConversionException; +import com.webauthn4j.data.AuthenticationData; +import com.webauthn4j.data.AuthenticationParameters; +import com.webauthn4j.data.AuthenticationRequest; import com.webauthn4j.data.PublicKeyCredentialDescriptor; import com.webauthn4j.data.PublicKeyCredentialRequestOptions; import com.webauthn4j.data.PublicKeyCredentialType; import com.webauthn4j.data.UserVerificationRequirement; -import com.webauthn4j.data.WebAuthnAuthenticationContext; import com.webauthn4j.data.attestation.authenticator.AuthenticatorData; import com.webauthn4j.data.client.Origin; import com.webauthn4j.data.client.challenge.Challenge; import com.webauthn4j.data.client.challenge.DefaultChallenge; import com.webauthn4j.server.ServerProperty; -import com.webauthn4j.validator.WebAuthnAuthenticationContextValidationResponse; -import com.webauthn4j.validator.WebAuthnAuthenticationContextValidator; +import com.webauthn4j.validator.exception.ValidationException; +import org.apache.commons.lang.ArrayUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; public class WebAuthnAuthenticationProcessor { + + private final WebAuthnManager webAuthnManager; - private Challenge challenge; + private final Challenge challenge; long timeout; public WebAuthnAuthenticationProcessor(String sessionId, long timeout) { this.challenge = new DefaultChallenge(sessionId.getBytes()); this.timeout = timeout; + webAuthnManager = WebAuthnManager.createNonStrictWebAuthnManager(); } - public PublicKeyCredentialRequestOptions requestCredentials(String username, HttpServletRequest request, - Set authenticators) throws AuthLoginException, JsonProcessingException { + public PublicKeyCredentialRequestOptions requestCredentials( + HttpServletRequest request, + Set authenticators) { String rpId = request.getServerName(); @@ -77,13 +81,14 @@ public PublicKeyCredentialRequestOptions requestCredentials(String username, Htt } public AuthenticatorData processCredentials(HttpServletRequest request, String idStr, - String authenticatorDataStr, String clientDataJSONStr, String signatureStr, - String userHandleStr, Set authenticators) { + String authenticatorDataStr, String clientDataJSONStr, String signatureStr, byte[] userHandle, + Set authenticators) { + byte[] id = Base64Utils.decodeFromUrlSafeString(idStr); byte[] clientDataJSON = Base64Utils.decodeFromUrlSafeString(clientDataJSONStr); byte[] authenticatorData = Base64Utils.decodeFromUrlSafeString(authenticatorDataStr); byte[] signature = Base64Utils.decodeFromUrlSafeString(signatureStr); - + Authenticator foundAuthenticator = null; for(Authenticator authenticator : authenticators ) { if(ArrayUtils.isEquals(authenticator.getAttestedCredentialData().getCredentialId(), id)) { @@ -91,35 +96,57 @@ public AuthenticatorData processCredentials(HttpServletRequest request, Strin break; } } - + if(foundAuthenticator == null) { return null; } - - Origin origin = new Origin(request.getScheme(), request.getServerName(), request.getServerPort()); + + Origin origin = new Origin(request.getHeader("Origin")); String rpId = request.getServerName(); - - - byte[] tokenBindingId = null; - ServerProperty serverProperty = new ServerProperty(origin, rpId, challenge, tokenBindingId); - boolean userVerificationRequired = false; - - WebAuthnAuthenticationContext authenticationContext = - new WebAuthnAuthenticationContext( - id, - clientDataJSON, - authenticatorData, - signature, - serverProperty, - userVerificationRequired - ); - - WebAuthnAuthenticationContextValidator webAuthnAuthenticationContextValidator = - new WebAuthnAuthenticationContextValidator(); - WebAuthnAuthenticationContextValidationResponse response = - webAuthnAuthenticationContextValidator.validate(authenticationContext, foundAuthenticator); - - return response.getAuthenticatorData(); + + byte[] tokenBindingId = null; + ServerProperty serverProperty = new ServerProperty(origin, rpId, challenge, tokenBindingId); + List allowCredentials = null; + boolean userVerificationRequired = false; + boolean userPresenceRequired = true; + + Authenticator authenticator = authenticators.stream().filter(a -> + Objects.deepEquals(a.getAttestedCredentialData().getCredentialId(), id)) + .findFirst().orElse(null); + + AuthenticationParameters authenticationParameters = + new AuthenticationParameters( + serverProperty, + authenticator, + allowCredentials, + userVerificationRequired, + userPresenceRequired + ); + + AuthenticationRequest authenticationRequest = new AuthenticationRequest( + id, userHandle, authenticatorData, clientDataJSON, null, signature + ); + + AuthenticationData authenticationData; + try { + authenticationData = webAuthnManager.parse(authenticationRequest); + } catch (DataConversionException e) { + // If you would like to handle WebAuthn data structure parse error, please catch DataConversionException + throw e; + } + try { + webAuthnManager.validate(authenticationData, authenticationParameters); + } catch (ValidationException e) { + // If you would like to handle WebAuthn data validation error, please catch ValidationException + throw e; + } +// please update the counter of the authenticator record TODO +// updateCounter( +// authenticationData.getCredentialId(), +// authenticationData.getAuthenticatorData().getSignCount() +// ); + + return authenticationData.getAuthenticatorData(); } } diff --git a/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnRegistration.java b/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnRegistration.java index 90ba067e08..de4611b378 100644 --- a/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnRegistration.java +++ b/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnRegistration.java @@ -11,46 +11,46 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyrighted [year] [name of copyright owner]". * - * Copyright 2019 3A-Systems LLC. All rights reserved. + * Copyright 2024 3A-Systems LLC. All rights reserved. */ package org.openidentityplatform.openam.authentication.modules.webauthn; -import java.security.Principal; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import javax.security.auth.Subject; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.PasswordCallback; -import javax.security.auth.callback.TextOutputCallback; -import javax.security.auth.login.LoginException; - -import org.apache.commons.lang.RandomStringUtils; -import org.apache.commons.lang.SerializationUtils; -import org.apache.commons.lang.StringUtils; -import org.forgerock.openam.authentication.modules.common.mapping.AccountProvider; -import org.forgerock.openam.authentication.modules.common.mapping.DefaultAccountProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.iplanet.dpro.session.service.InternalSession; import com.iplanet.sso.SSOException; +import com.iplanet.sso.SSOToken; +import com.iplanet.sso.SSOTokenManager; +import com.sun.identity.authentication.service.AuthD; +import com.sun.identity.authentication.service.AuthException; import com.sun.identity.authentication.spi.AMLoginModule; import com.sun.identity.authentication.spi.AuthLoginException; import com.sun.identity.authentication.util.ISAuthConstants; import com.sun.identity.idm.AMIdentity; import com.sun.identity.idm.IdRepoException; +import com.sun.identity.idm.IdType; import com.sun.identity.shared.datastruct.CollectionHelper; +import com.sun.identity.shared.debug.Debug; +import com.sun.identity.sm.DNMapper; import com.webauthn4j.authenticator.Authenticator; import com.webauthn4j.data.AttestationConveyancePreference; import com.webauthn4j.data.AuthenticatorAttachment; import com.webauthn4j.data.PublicKeyCredentialCreationOptions; +import org.apache.commons.lang.SerializationUtils; +import org.apache.commons.lang.StringUtils; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.TextOutputCallback; +import javax.security.auth.login.LoginException; +import java.security.Principal; +import java.util.Collections; +import java.util.Map; +import java.util.Set; /** * @@ -58,43 +58,42 @@ */ public class WebAuthnRegistration extends AMLoginModule { - final static Logger logger = LoggerFactory.getLogger(WebAuthnRegistration.class); + protected Debug debug = null; final static ObjectMapper mapper = new ObjectMapper(); static { mapper.setSerializationInclusion(Include.NON_NULL); } + + public WebAuthnRegistration() { + debug = Debug.getInstance("amWebAuthnRegistration"); + } private Map sharedState = null; private final static int LOGIN_REQUEST_CREDENTIALS_STATE = 2; - private final static int CHALLENGE_ID_CB_INDEX = 0; - private final static int CHALLENGE_TYPE_CB_INDEX = 1; - private final static int CHALLENGE_ATTESTATION_CB_INDEX = 2; - private final static int CHALLENGE_CLIENT_DATA_CB_INDEX = 3; - - private final static int CREDENTIAL_REQUEST_CB_INDEX = 4; + private final static int CREDENTIALS_CB_INDEX = 0; + private final static int CREDENTIAL_REQUEST_CB_INDEX = 1; private WebAuthnRegistrationProcessor webAuthnRegistrationProcessor = null; private AttestationConveyancePreference attestation = null; private AuthenticatorAttachment authenticatorAttachment = null; - private String username = null; + protected String userId = null; private long timeout; private String userAttribute = null; - private AccountProvider accountProvider = new DefaultAccountProvider(); - private int authLevel = 0; - + + @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public void init(Subject subject, Map sharedState, Map options) { this.sharedState = sharedState; - + this.attestation = AttestationConveyancePreference.create( CollectionHelper.getMapAttr(options, WebAuthnRegistration.class.getName().concat(".attestation"), @@ -120,43 +119,52 @@ public void init(Subject subject, Map sharedState, Map options) { this.authLevel = Integer.parseInt(CollectionHelper.getMapAttr(options, WebAuthnRegistration.class.getName().concat(".authlevel"), "0")); + + + webAuthnRegistrationProcessor = new WebAuthnRegistrationProcessor( + getSessionId(), + attestation, authenticatorAttachment, timeout); + + initUserId(); + } + + protected void initUserId() { + try { + SSOTokenManager mgr = SSOTokenManager.getInstance(); + InternalSession is = getLoginState(WebAuthnRegistration.class.getName()).getOldSession(); + if (is == null) { + throw new AuthLoginException("amAuth", "noInternalSession", null); + } + SSOToken token = mgr.createSSOToken(is.getID().toString()); + userId = token.getProperty("UserToken"); + if (debug.messageEnabled()) { + debug.message("WebAuthnRegistration.initUserId() : Username from SSOToken : " + userId); + } + } catch (Exception e) { + debug.error("WebAuthnRegistration.initUserId() : Exception", e); + } } @Override public int process(Callback[] callbacks, int state) throws LoginException { - - if(webAuthnRegistrationProcessor == null) { - webAuthnRegistrationProcessor = new WebAuthnRegistrationProcessor( - getSessionId(), - attestation, authenticatorAttachment, timeout); + if (StringUtils.isBlank(userId)) { + throw new AuthLoginException("amAuth", "noUserName", null); } - + try { switch (state) { case ISAuthConstants.LOGIN_START: - if(callbacks == null) { //no callback init authentication - if(username == null && sharedState.containsKey(getUserKey())) { - username = (String)sharedState.get(getUserKey()); - return requestCredentials(); - } else { - return ISAuthConstants.LOGIN_START; - } - } - else { - //getting username - username = ((NameCallback)callbacks[0]).getName(); - return requestCredentials(); - } + return requestCredentials(); case LOGIN_REQUEST_CREDENTIALS_STATE: return processCredentials(callbacks); default: break; } } catch(AuthLoginException e) { - logger.error("process: AuthLoginException {}", e.toString()); + debug.error("process: AuthLoginException {}", e.toString()); throw e; } catch(Exception e) { - logger.error("process: Exception {}", e.toString()); + debug.error("process: Exception {}", e.toString()); throw new AuthLoginException(e); } @@ -165,7 +173,7 @@ public int process(Callback[] callbacks, int state) throws LoginException { public int requestCredentials() throws AuthLoginException, JsonProcessingException { - PublicKeyCredentialCreationOptions credentialCreationOptions = webAuthnRegistrationProcessor.requestCredentials(username, getHttpServletRequest()); + PublicKeyCredentialCreationOptions credentialCreationOptions = webAuthnRegistrationProcessor.requestCredentials(userId, getHttpServletRequest()); String credentialCreationOptionsString = mapper.writeValueAsString(credentialCreationOptions); TextOutputCallback credentialCreationOptionsCallback = new TextOutputCallback(TextOutputCallback.INFORMATION, credentialCreationOptionsString); @@ -175,12 +183,19 @@ public int requestCredentials() throws AuthLoginException, JsonProcessingExcepti } private int processCredentials(Callback[] callbacks) throws AuthLoginException { - String id = new String(((PasswordCallback) callbacks[CHALLENGE_ID_CB_INDEX]).getPassword()); - String type = new String(((PasswordCallback) callbacks[CHALLENGE_TYPE_CB_INDEX]).getPassword()); - String attestationObjectStr = new String(((PasswordCallback) callbacks[CHALLENGE_ATTESTATION_CB_INDEX]).getPassword()); - String clientDataJSONStr = new String(((PasswordCallback) callbacks[CHALLENGE_CLIENT_DATA_CB_INDEX]).getPassword()); - - Authenticator authenticator = webAuthnRegistrationProcessor.processCredentials(id, type, attestationObjectStr, clientDataJSONStr, getHttpServletRequest()); + final String credentialsStr = new String(((PasswordCallback) callbacks[CREDENTIALS_CB_INDEX]).getPassword()); + final Map credentials; + try { + credentials = mapper.readValue(credentialsStr, new TypeReference>() {}); + } catch (Exception e) { + debug.error("invalid credentials data: " + credentialsStr, e); + throw new AuthLoginException(e); + } + String attestationObjectStr = credentials.get("attestationObject"); + String clientDataJSONStr = credentials.get("clientDataJSON"); + + Authenticator authenticator = webAuthnRegistrationProcessor.processCredentials(attestationObjectStr, clientDataJSONStr, getHttpServletRequest()); + save(authenticator); setAuthLevel(authLevel); @@ -188,34 +203,27 @@ private int processCredentials(Callback[] callbacks) throws AuthLoginException { return ISAuthConstants.LOGIN_SUCCEED; } - @SuppressWarnings("unchecked") - protected void save(Authenticator authenticator) throws AuthLoginException { - - Map> attributes = new HashMap<>(); - attributes.put("uid", Collections.singleton(username)); - String randomPassword = RandomStringUtils.random(20, true, true); - attributes.put("userPassword", Collections.singleton(randomPassword)); - attributes.put("inetuserstatus", Collections.singleton("Active")); - AMIdentity user = accountProvider.provisionUser(getAMIdentityRepository(getRequestOrg()), attributes); - - try { - user.setActiveStatus(true); - Set authenticators = user.getAttribute(userAttribute); + protected void save(Authenticator authenticator) throws AuthLoginException { + String realm = DNMapper.orgNameToRealmName(getRequestOrg()); + try { + AMIdentity id = AuthD.getAuth().getIdentity(IdType.USER, userId, realm); + Set authenticators = id.getAttribute(userAttribute); byte[] bytes = SerializationUtils.serialize(authenticator); String authStr = Base64Utils.encodeToUrlSafeString(bytes); authenticators.add(authStr); - user.setAttributes(Collections.singletonMap(userAttribute, authenticators)); - user.store(); - } catch (SSOException | IdRepoException e) { - logger.error("save: error update user : {}", e); + id.setAttributes(Collections.singletonMap(userAttribute, authenticators)); + id.store(); + } catch (SSOException | IdRepoException | AuthException e) { + debug.error("WebAuthnRegistration: save(): error update user : {}", e); throw new AuthLoginException(e); } - } @Override public Principal getPrincipal() { - return new WebAuthnPrincipal(username); + return new WebAuthnPrincipal(userId); } + + } diff --git a/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnRegistrationProcessor.java b/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnRegistrationProcessor.java index 915fc81ff8..bed88aa3bc 100644 --- a/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnRegistrationProcessor.java +++ b/openam-authentication/openam-auth-webauthn/src/main/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnRegistrationProcessor.java @@ -11,21 +11,15 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyrighted [year] [name of copyright owner]". * - * Copyright 2019 3A-Systems LLC. All rights reserved. + * Copyright 2024 3A-Systems LLC. All rights reserved. */ package org.openidentityplatform.openam.authentication.modules.webauthn; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import javax.servlet.http.HttpServletRequest; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.sun.identity.authentication.spi.AuthLoginException; +import com.webauthn4j.WebAuthnManager; import com.webauthn4j.authenticator.Authenticator; import com.webauthn4j.authenticator.AuthenticatorImpl; +import com.webauthn4j.converter.exception.DataConversionException; import com.webauthn4j.data.AttestationConveyancePreference; import com.webauthn4j.data.AuthenticatorAttachment; import com.webauthn4j.data.AuthenticatorSelectionCriteria; @@ -35,47 +29,45 @@ import com.webauthn4j.data.PublicKeyCredentialRpEntity; import com.webauthn4j.data.PublicKeyCredentialType; import com.webauthn4j.data.PublicKeyCredentialUserEntity; +import com.webauthn4j.data.RegistrationData; +import com.webauthn4j.data.RegistrationParameters; +import com.webauthn4j.data.RegistrationRequest; import com.webauthn4j.data.UserVerificationRequirement; -import com.webauthn4j.data.WebAuthnRegistrationContext; import com.webauthn4j.data.attestation.statement.COSEAlgorithmIdentifier; import com.webauthn4j.data.client.Origin; import com.webauthn4j.data.client.challenge.Challenge; import com.webauthn4j.data.client.challenge.DefaultChallenge; import com.webauthn4j.server.ServerProperty; -import com.webauthn4j.validator.WebAuthnRegistrationContextValidationResponse; -import com.webauthn4j.validator.WebAuthnRegistrationContextValidator; +import com.webauthn4j.validator.exception.ValidationException; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; public class WebAuthnRegistrationProcessor { - - private Challenge challenge; - private AuthenticatorAttachment authenticatorAttachment; - private AttestationConveyancePreference attestation; - private long timeout; - - public WebAuthnRegistrationProcessor(String sessionId, - AttestationConveyancePreference attestation, - AuthenticatorAttachment authenticatorAttachment, - long timeout) { - - this.challenge = new DefaultChallenge(sessionId.getBytes()); - this.attestation = attestation; - this.authenticatorAttachment = authenticatorAttachment; - this.timeout = timeout; - - } - - public PublicKeyCredentialCreationOptions requestCredentials(String username, HttpServletRequest request) throws AuthLoginException, JsonProcessingException { - - String rpId = request.getServerName(); - - PublicKeyCredentialRpEntity rp = new PublicKeyCredentialRpEntity(rpId, rpId); + private final WebAuthnManager webAuthnManager; + private final List pubKeyCredParams; + private Challenge challenge; + private AuthenticatorAttachment authenticatorAttachment; + private AttestationConveyancePreference attestation; + private long timeout; - PublicKeyCredentialUserEntity user = new PublicKeyCredentialUserEntity(username.getBytes(), - username, - username); - - List pubKeyCredParams = new ArrayList(); + + public WebAuthnRegistrationProcessor(String sessionId, + AttestationConveyancePreference attestation, + AuthenticatorAttachment authenticatorAttachment, + long timeout) { + + this.challenge = new DefaultChallenge(sessionId.getBytes()); + this.attestation = attestation; + this.authenticatorAttachment = authenticatorAttachment; + this.timeout = timeout; + + webAuthnManager = WebAuthnManager.createNonStrictWebAuthnManager(); + + pubKeyCredParams = new ArrayList<>(); pubKeyCredParams.add( new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.ES256)); pubKeyCredParams.add( @@ -87,9 +79,20 @@ public PublicKeyCredentialCreationOptions requestCredentials(String username, Ht pubKeyCredParams.add( new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.RS384)); pubKeyCredParams.add( - new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.RS512)); + new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.RS512)); + } + + + public PublicKeyCredentialCreationOptions requestCredentials(String username, HttpServletRequest request) { + + String rpId = request.getServerName(); + + PublicKeyCredentialRpEntity rp = new PublicKeyCredentialRpEntity(rpId, rpId); + + PublicKeyCredentialUserEntity user = new PublicKeyCredentialUserEntity(username.getBytes(), + username, + username); - UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED; List excludeCredentials = Collections.emptyList(); @@ -97,7 +100,7 @@ public PublicKeyCredentialCreationOptions requestCredentials(String username, Ht AuthenticatorSelectionCriteria authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria( this.authenticatorAttachment, - false, + true, userVerificationRequirement); PublicKeyCredentialCreationOptions credentialCreationOptions = new PublicKeyCredentialCreationOptions( @@ -111,40 +114,49 @@ public PublicKeyCredentialCreationOptions requestCredentials(String username, Ht this.attestation, null ); - + return credentialCreationOptions; } - public Authenticator processCredentials(String id, String type, String attestationObjectStr, - String clientDataJSONStr, HttpServletRequest request) - throws AuthLoginException { + public Authenticator processCredentials(String attestationObjectStr, + String clientDataJSONStr, HttpServletRequest request) { - Origin origin = new Origin(request.getScheme(), request.getServerName(), request.getServerPort()); + Origin origin = new Origin(request.getHeader("Origin")); String rpId = request.getServerName(); byte[] clientDataJSON = Base64Utils.decodeFromUrlSafeString(clientDataJSONStr); byte[] attestationObject = Base64Utils.decodeFromUrlSafeString(attestationObjectStr); byte[] tokenBindingId = null; - ServerProperty serverProperty = new ServerProperty(origin, rpId, challenge, tokenBindingId); - boolean userVerificationRequired = false; + ServerProperty serverProperty = new ServerProperty(origin, rpId, challenge, tokenBindingId); + boolean userVerificationRequired = false; + boolean userPresenceRequired = true; - WebAuthnRegistrationContext registrationContext = new WebAuthnRegistrationContext( - clientDataJSON, - attestationObject, - serverProperty, - userVerificationRequired - ); - WebAuthnRegistrationContextValidator webAuthnRegistrationContextValidator = - WebAuthnRegistrationContextValidator.createNonStrictRegistrationContextValidator(); + RegistrationRequest registrationRequest = new RegistrationRequest(attestationObject, clientDataJSON); + + RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, + pubKeyCredParams, + userVerificationRequired, + userPresenceRequired); + RegistrationData registrationData; + try { + registrationData = webAuthnManager.parse(registrationRequest); + } catch (DataConversionException e) { + throw e; + } - WebAuthnRegistrationContextValidationResponse response = webAuthnRegistrationContextValidator.validate(registrationContext); + try { + webAuthnManager.validate(registrationData, registrationParameters); + } catch (ValidationException e) { + throw e; + } Authenticator authenticator = - new AuthenticatorImpl( // You may create your own Authenticator implementation to save friendly authenticator name - response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData(), - response.getAttestationObject().getAttestationStatement(), - response.getAttestationObject().getAuthenticatorData().getSignCount() + new AuthenticatorImpl( + registrationData.getAttestationObject().getAuthenticatorData().getAttestedCredentialData(), + registrationData.getAttestationObject().getAttestationStatement(), + registrationData.getAttestationObject().getAuthenticatorData().getSignCount(), + registrationData.getTransports() ); return authenticator; } diff --git a/openam-authentication/openam-auth-webauthn/src/test/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnAuthenticationTest.java b/openam-authentication/openam-auth-webauthn/src/test/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnAuthenticationTest.java index a46036c235..d28b180c16 100644 --- a/openam-authentication/openam-auth-webauthn/src/test/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnAuthenticationTest.java +++ b/openam-authentication/openam-auth-webauthn/src/test/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnAuthenticationTest.java @@ -11,7 +11,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyrighted [year] [name of copyright owner]". * - * Copyright 2019 3A-Systems LLC. All rights reserved. + * Copyright 2024 3A-Systems LLC. All rights reserved. */ package org.openidentityplatform.openam.authentication.modules.webauthn; @@ -30,6 +30,7 @@ import javax.security.auth.callback.NameCallback; import javax.servlet.http.HttpServletRequest; +import com.sun.identity.idm.AMIdentity; import org.mockito.Mockito; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -53,13 +54,13 @@ public void initMocks() throws AuthLoginException { when(webAuthnAuthentication.getHttpServletRequest()).thenReturn(httpServletRequest); when(webAuthnAuthentication.getSessionId()).thenReturn("87DCE7CF5F9DB00AC98367CA8640884F"); doNothing().when(webAuthnAuthentication).replaceCallback(anyInt(), anyInt(), any(Callback.class)); - doReturn(Collections.emptySet()).when(webAuthnAuthentication).loadAuthenticators(); + doReturn(Collections.emptySet()).when(webAuthnAuthentication).loadAuthenticators(any(AMIdentity.class)); } @Test public void testProcessRequestUsername() throws Exception { webAuthnAuthentication.init(null, Collections.EMPTY_MAP, Collections.EMPTY_MAP); - assertEquals(1, webAuthnAuthentication.process(null, 1)); + assertEquals(webAuthnAuthentication.process(null, 1), 2); } @Test diff --git a/openam-authentication/openam-auth-webauthn/src/test/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnRegistrationTest.java b/openam-authentication/openam-auth-webauthn/src/test/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnRegistrationTest.java index 0211ed4802..b2d5197fc5 100644 --- a/openam-authentication/openam-auth-webauthn/src/test/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnRegistrationTest.java +++ b/openam-authentication/openam-auth-webauthn/src/test/java/org/openidentityplatform/openam/authentication/modules/webauthn/WebAuthnRegistrationTest.java @@ -11,7 +11,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyrighted [year] [name of copyright owner]". * - * Copyright 2019 3A-Systems LLC. All rights reserved. + * Copyright 2024 3A-Systems LLC. All rights reserved. */ package org.openidentityplatform.openam.authentication.modules.webauthn; @@ -46,22 +46,26 @@ public class WebAuthnRegistrationTest { @BeforeMethod public void initMocks() throws AuthLoginException { - HttpServletRequest httpServletRequest = mock(HttpServletRequest.class); + HttpServletRequest httpServletRequest = mock(HttpServletRequest.class); when(httpServletRequest.getScheme()).thenReturn("http"); when(httpServletRequest.getServerName()).thenReturn("localhost"); when(httpServletRequest.getServerPort()).thenReturn(8080); + when(httpServletRequest.getHeader("Origin")).thenReturn("http://localhost:8080"); webAuthnRegistration = mock(WebAuthnRegistration.class, Mockito.CALLS_REAL_METHODS); when(webAuthnRegistration.getHttpServletRequest()).thenReturn(httpServletRequest); when(webAuthnRegistration.getSessionId()).thenReturn("87DCE7CF5F9DB00AC98367CA8640884F"); doNothing().when(webAuthnRegistration).replaceCallback(anyInt(), anyInt(), any(Callback.class)); doNothing().when(webAuthnRegistration).save(any(Authenticator.class)); + doNothing().when(webAuthnRegistration).initUserId(); + webAuthnRegistration.userId = "test"; + webAuthnRegistration.init(null, Collections.emptyMap(), Collections.emptyMap()); } @Test public void testProcessRequestUsername() throws Exception { webAuthnRegistration.init(null, Collections.EMPTY_MAP, Collections.EMPTY_MAP); - assertEquals(1, webAuthnRegistration.process(null, 1)); + assertEquals(webAuthnRegistration.process(null, 1), 2); } @Test @@ -82,22 +86,19 @@ public void testProcessRequestCredentials() throws Exception { @Test public void testProcessCredentials() throws Exception { - PasswordCallback idCallback = new PasswordCallback("id", false); - idCallback.setPassword("Dt46gcUIV08YHRo4tXmt85Ie8Ihiw2MDr5ARgPhKG2ByDhmH0jzQbivWALXGM0RKM0LWO9mI7rtX1KNnzhhyLVCE_3F1V-ePT2M-HNu-91bcBeZ_CHzrAVksE-wP2NCpJSp_dMlxV1-rPUampHGoMRMyU_7Xi9Rw8Scl_9jqRitCRD3XbZWOCOY8B7T0j-EIAGFrIei30YwTMxQBndBDu8WcYUA60yM0BwwR482seQSCHBfh8maYy1GEyiP8bpeYjZHuDouel5EKIvc2pa5esGVuVY1cBQaMfZh4DVMCgnJlb8PvoOfmKJhUXTStVdDXrtplK9Id-AWh2UdoMK-T".toCharArray()); + PasswordCallback idCallback = new PasswordCallback("data", false); + final String credentialId = "Dt46gcUIV08YHRo4tXmt85Ie8Ihiw2MDr5ARgPhKG2ByDhmH0jzQbivWALXGM0RKM0LWO9mI7rtX1KNnzhhyLVCE_3F1V-ePT2M-HNu-91bcBeZ_CHzrAVksE-wP2NCpJSp_dMlxV1-rPUampHGoMRMyU_7Xi9Rw8Scl_9jqRitCRD3XbZWOCOY8B7T0j-EIAGFrIei30YwTMxQBndBDu8WcYUA60yM0BwwR482seQSCHBfh8maYy1GEyiP8bpeYjZHuDouel5EKIvc2pa5esGVuVY1cBQaMfZh4DVMCgnJlb8PvoOfmKJhUXTStVdDXrtplK9Id-AWh2UdoMK-T"; + final String attestationObject = "o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIgbdJQ2Q0udhpZxTSwCM00TvxDTPhQ2lxRafOkQgvd8IkCIQDnjiJiGygHNbsCm-yEznz8RkdR94YDCqp5Fpcp-g7wRWN4NWOBWQF1MIIBcTCCARagAwIBAgIJAIqK93XCOr_GMAoGCCqGSM49BAMCMBMxETAPBgNVBAMMCFNvZnQgVTJGMB4XDTE3MTAyMDIxNTEzM1oXDTI3MTAyMDIxNTEzM1owEzERMA8GA1UEAwwIU29mdCBVMkYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ5gjPmSDFBJap5rwDMdyqO4lCcWqQXxXtHBN-S-zt6ytC3amquoctXGuOOKZikTkT_gX8LFXVqmMZIcvC4EziGo1MwUTAdBgNVHQ4EFgQU8bJw2i1BjqI2uvqQWqNempxTxD4wHwYDVR0jBBgwFoAU8bJw2i1BjqI2uvqQWqNempxTxD4wDwYDVR0TAQH_BAUwAwEB_zAKBggqhkjOPQQDAgNJADBGAiEApFdcnvfziaAunldkAvHDwNViRH461fZv_6tFlbYPGEwCIQCS1PM8fMOKTgdr3hpqeQq_ysQK8NJZtPbFADEk8effHWhhdXRoRGF0YVkBg0mWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAAAAAAAAAAAAAAAAAAAAAAAAAP8O3jqBxQhXTxgdGji1ea3zkh7wiGLDYwOvkBGA-EobYHIOGYfSPNBuK9YAtcYzREozQtY72Yjuu1fUo2fOGHItUIT_cXVX549PYz4c2773VtwF5n8IfOsBWSwT7A_Y0KklKn90yXFXX6s9RqakcagxEzJT_teL1HDxJyX_2OpGK0JEPddtlY4I5jwHtPSP4QgAYWsh6LfRjBMzFAGd0EO7xZxhQDrTIzQHDBHjzax5BIIcF-HyZpjLUYTKI_xul5iNke4Oi56XkQoi9zalrl6wZW5VjVwFBox9mHgNUwKCcmVvw--g5-YomFRdNK1V0Neu2mUr0h34BaHZR2gwr5OlAQIDJiABIVggFIRwFmWDe2G6Vap-47mKkZJy0fjxw7vaWjy3nUlKxjUiWCCXaby_67nTqm3pDPHI8mI2dkZzZjS9beegzhdkL6Hddg"; + final String clientDataJSON = "eyJjaGFsbGVuZ2UiOiJPRGRFUTBVM1EwWTFSamxFUWpBd1FVTTVPRE0yTjBOQk9EWTBNRGc0TkVZIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9"; + final String credentials = String.format("{\"credentialId\": \"%s\", " + + "\"attestationObject\": \"%s\", " + + "\"clientDataJSON\": \"%s\" }", credentialId, attestationObject, clientDataJSON); + idCallback.setPassword(credentials.toCharArray()); - PasswordCallback typeCallback = new PasswordCallback("type", false); - typeCallback.setPassword("public-key".toCharArray()); - - PasswordCallback attestationObjectCallback = new PasswordCallback("attestation object", false); - attestationObjectCallback.setPassword("o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIgbdJQ2Q0udhpZxTSwCM00TvxDTPhQ2lxRafOkQgvd8IkCIQDnjiJiGygHNbsCm-yEznz8RkdR94YDCqp5Fpcp-g7wRWN4NWOBWQF1MIIBcTCCARagAwIBAgIJAIqK93XCOr_GMAoGCCqGSM49BAMCMBMxETAPBgNVBAMMCFNvZnQgVTJGMB4XDTE3MTAyMDIxNTEzM1oXDTI3MTAyMDIxNTEzM1owEzERMA8GA1UEAwwIU29mdCBVMkYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ5gjPmSDFBJap5rwDMdyqO4lCcWqQXxXtHBN-S-zt6ytC3amquoctXGuOOKZikTkT_gX8LFXVqmMZIcvC4EziGo1MwUTAdBgNVHQ4EFgQU8bJw2i1BjqI2uvqQWqNempxTxD4wHwYDVR0jBBgwFoAU8bJw2i1BjqI2uvqQWqNempxTxD4wDwYDVR0TAQH_BAUwAwEB_zAKBggqhkjOPQQDAgNJADBGAiEApFdcnvfziaAunldkAvHDwNViRH461fZv_6tFlbYPGEwCIQCS1PM8fMOKTgdr3hpqeQq_ysQK8NJZtPbFADEk8effHWhhdXRoRGF0YVkBg0mWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAAAAAAAAAAAAAAAAAAAAAAAAAP8O3jqBxQhXTxgdGji1ea3zkh7wiGLDYwOvkBGA-EobYHIOGYfSPNBuK9YAtcYzREozQtY72Yjuu1fUo2fOGHItUIT_cXVX549PYz4c2773VtwF5n8IfOsBWSwT7A_Y0KklKn90yXFXX6s9RqakcagxEzJT_teL1HDxJyX_2OpGK0JEPddtlY4I5jwHtPSP4QgAYWsh6LfRjBMzFAGd0EO7xZxhQDrTIzQHDBHjzax5BIIcF-HyZpjLUYTKI_xul5iNke4Oi56XkQoi9zalrl6wZW5VjVwFBox9mHgNUwKCcmVvw--g5-YomFRdNK1V0Neu2mUr0h34BaHZR2gwr5OlAQIDJiABIVggFIRwFmWDe2G6Vap-47mKkZJy0fjxw7vaWjy3nUlKxjUiWCCXaby_67nTqm3pDPHI8mI2dkZzZjS9beegzhdkL6Hddg".toCharArray()); - - PasswordCallback clientDataJSONCallback = new PasswordCallback("client data json", false); - clientDataJSONCallback.setPassword("eyJjaGFsbGVuZ2UiOiJPRGRFUTBVM1EwWTFSamxFUWpBd1FVTTVPRE0yTjBOQk9EWTBNRGc0TkVZIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9".toCharArray()); - - TextOutputCallback credentials = new TextOutputCallback(TextOutputCallback.INFORMATION, "credentials"); + + TextOutputCallback credentialsCallback = new TextOutputCallback(TextOutputCallback.INFORMATION, "credentials"); - Callback[] callbacks = new Callback[] {idCallback, typeCallback, - attestationObjectCallback, clientDataJSONCallback, credentials}; + Callback[] callbacks = new Callback[] {idCallback, credentialsCallback}; assertEquals(ISAuthConstants.LOGIN_SUCCEED, webAuthnRegistration.process(callbacks, 2)); } diff --git a/openam-server-only/src/main/webapp/assets/js/webauthn.js b/openam-server-only/src/main/webapp/assets/js/webauthn.js index 012712b446..f6e41f8595 100644 --- a/openam-server-only/src/main/webapp/assets/js/webauthn.js +++ b/openam-server-only/src/main/webapp/assets/js/webauthn.js @@ -2,15 +2,19 @@ function bufferDecode(value) { return Uint8Array.from(atob(value), c => c.charCodeAt(0)); } -function bufferEncode(value) { - return Base64.fromByteArray(value) - .replace(/\+/g, "-") +function bufferEncode(bytes) { + let binary = '' + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode( bytes[ i ] ); + } + return window.btoa( binary ).replace(/\+/g, "-") .replace(/\//g, "_") - .replace(/=/g, ""); + .replace(/=/g, "") } function processRegistrationChallenge() { - var challengeStr = document.querySelector(".TextOutputCallback_0").innerText; + var challengeStr = getRegistrationChallenge(); var challenge = JSON.parse(challengeStr); challenge.challenge = bufferDecode(challenge.challenge.value); challenge.user.id = bufferDecode(challenge.user.id); @@ -24,17 +28,38 @@ function processRegistrationChallenge() { ); } -function register(credential) { - document.getElementById("IDToken1").value = credential.id; - document.getElementById('IDToken2').value = credential.type; - document.getElementById('IDToken3').value = bufferEncode( new Uint8Array(credential.response.attestationObject)); - document.getElementById('IDToken4').value = bufferEncode( new Uint8Array(credential.response.clientDataJSON)); +function getRegistrationChallenge() { + var querySelector = ".TextOutputCallback_0"; + if(isXUI()) { + querySelector = "#callback_3"; + } + return document.querySelector(querySelector).innerText; +} - document.querySelector("form").submit(); +function isXUI() { + return !!window.requirejs; +} + +function register(credential) { + var idToken1Sel = "IDToken1"; + var idToken2Sel = "IDToken2"; + var idToken3Sel = "IDToken3"; + var buttonSel = "[name='Login.Submit']"; + if(isXUI()) { + idToken1Sel = "idToken1"; + idToken2Sel = "idToken2"; + idToken3Sel = "idToken3"; + buttonSel = "#loginButton_0"; + } + document.getElementById(idToken1Sel).value = credential.id; + document.getElementById(idToken2Sel).value = bufferEncode( new Uint8Array(credential.response.attestationObject)); + document.getElementById(idToken3Sel).value = bufferEncode( new Uint8Array(credential.response.clientDataJSON)); + document.querySelector(buttonSel).click(); } + function processAuthenticationChallenge() { - var credentialsStr = document.querySelector(".TextOutputCallback_0").innerText; + var credentialsStr = getAuthenticationChallenge(); var credentials = JSON.parse(credentialsStr); credentials.challenge = bufferDecode(credentials.challenge.value); credentials.allowCredentials.forEach(function (allowCredential, i) { @@ -57,14 +82,36 @@ function assert(assertion) { var clientDataJSON = new Uint8Array(assertion.response.clientDataJSON); var signature = new Uint8Array(assertion.response.signature); var userHandle = new Uint8Array(assertion.response.userHandle); - var rawId = new Uint8Array(assertion.rawId); - document.getElementById("IDToken1").value = assertion.id; - document.getElementById('IDToken2').value = bufferEncode( new Uint8Array(authenticatorData)); - document.getElementById('IDToken3').value = bufferEncode( new Uint8Array(clientDataJSON)); - document.getElementById('IDToken4').value = bufferEncode( new Uint8Array(signature)); - document.getElementById('IDToken5').value = bufferEncode(userHandle); + var idToken1Sel = "IDToken1"; + var idToken2Sel = "IDToken2"; + var idToken3Sel = "IDToken3"; + var idToken4Sel = "IDToken4"; + var idToken5Sel = "IDToken5"; + var buttonSel = "[name='Login.Submit']"; + if(isXUI()) { + idToken1Sel = "idToken1"; + idToken2Sel = "idToken2"; + idToken3Sel = "idToken3"; + idToken4Sel = "idToken4"; + idToken5Sel = "idToken5"; + buttonSel = "#loginButton_0"; + } + + document.getElementById(idToken1Sel).value = assertion.id; + document.getElementById(idToken2Sel).value = bufferEncode( new Uint8Array(authenticatorData)); + document.getElementById(idToken3Sel).value = bufferEncode( new Uint8Array(clientDataJSON)); + document.getElementById(idToken4Sel).value = bufferEncode( new Uint8Array(signature)); + document.getElementById(idToken5Sel).value = bufferEncode(userHandle); - document.querySelector("form").submit(); + document.querySelector(buttonSel).click(); + +} +function getAuthenticationChallenge() { + var querySelector = ".TextOutputCallback_0"; + if(isXUI()) { + querySelector = "#callback_5"; + } + return document.querySelector(querySelector).innerText; } \ No newline at end of file diff --git a/openam-server-only/src/main/webapp/config/auth/default/WebAuthnAuthentication.xml b/openam-server-only/src/main/webapp/config/auth/default/WebAuthnAuthentication.xml index dad36d5267..7456ddd83d 100644 --- a/openam-server-only/src/main/webapp/config/auth/default/WebAuthnAuthentication.xml +++ b/openam-server-only/src/main/webapp/config/auth/default/WebAuthnAuthentication.xml @@ -28,34 +28,26 @@ --> - + - - User Name: - + + + + Log In + + + - + - Id - - - Authentication Data - - - Client Data Json - - - Signature - - - User Handle + Credentials CREDENTIALS_PLACEHOLDER diff --git a/openam-server-only/src/main/webapp/config/auth/default/WebAuthnRegistration.xml b/openam-server-only/src/main/webapp/config/auth/default/WebAuthnRegistration.xml index 5dbbd1c3b8..71cfcbdfc2 100644 --- a/openam-server-only/src/main/webapp/config/auth/default/WebAuthnRegistration.xml +++ b/openam-server-only/src/main/webapp/config/auth/default/WebAuthnRegistration.xml @@ -28,31 +28,26 @@ --> - + - - User Name: - + + + + Register + + + - + - Id - - - Type - - - Attestation Object - - - Client Data Json + Credentials CREDENTIALS_PLACEHOLDER diff --git a/openam-ui/openam-ui-ria/src/main/resources/templates/openam/authn/WebAuthnAuthentication2.html b/openam-ui/openam-ui-ria/src/main/resources/templates/openam/authn/WebAuthnAuthentication2.html new file mode 100644 index 0000000000..e1d20eb5d9 --- /dev/null +++ b/openam-ui/openam-ui-ria/src/main/resources/templates/openam/authn/WebAuthnAuthentication2.html @@ -0,0 +1,36 @@ +
+ + + {{#if reqs.header}} + + {{/if}} + + +
diff --git a/openam-ui/openam-ui-ria/src/main/resources/templates/openam/authn/WebAuthnRegistration2.html b/openam-ui/openam-ui-ria/src/main/resources/templates/openam/authn/WebAuthnRegistration2.html new file mode 100644 index 0000000000..39c48fa63d --- /dev/null +++ b/openam-ui/openam-ui-ria/src/main/resources/templates/openam/authn/WebAuthnRegistration2.html @@ -0,0 +1,36 @@ +
+ + + {{#if reqs.header}} + + {{/if}} + + +