Skip to content

Commit

Permalink
WebAuthn implementation for XUI (#750)
Browse files Browse the repository at this point in the history
* WebAuthn XUI implementation

* WebAuthn pass credentials in a single callback
  • Loading branch information
maximthomas authored May 17, 2024
1 parent 058d7f1 commit c7426c4
Show file tree
Hide file tree
Showing 12 changed files with 495 additions and 346 deletions.
4 changes: 2 additions & 2 deletions openam-authentication/openam-auth-webauthn/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
Expand All @@ -24,7 +24,7 @@
<version>14.8.5-SNAPSHOT</version>
</parent>
<properties>
<webauthn4j.version>0.9.5.RELEASE</webauthn4j.version>
<webauthn4j.version>0.21.9.RELEASE</webauthn4j.version>
</properties>

<!-- Component Definition -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
*
Expand All @@ -58,25 +56,20 @@
*/
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 {
mapper.setSerializationInclusion(Include.NON_NULL);
}

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;

Expand All @@ -87,13 +80,10 @@ public class WebAuthnAuthentication extends AMLoginModule {
private String userAttribute = null;

private long timeout;

private AccountProvider accountProvider = new DefaultAccountProvider();

private Set<Authenticator> authenticators;

private int authLevel = 0;



@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public void init(Subject subject, Map sharedState, Map options) {
Expand All @@ -109,53 +99,39 @@ 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);
}

return ISAuthConstants.LOGIN_START;
}

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);
Expand All @@ -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<String, String> credentials;
try {
credentials = mapper.readValue(credentialsStr, new TypeReference<Map<String, String>>() {});
} 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<Authenticator> 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;
Expand All @@ -194,21 +191,18 @@ public Principal getPrincipal() {
}

@SuppressWarnings("unchecked")
protected Set<Authenticator> loadAuthenticators() throws AuthLoginException {
protected Set<Authenticator> loadAuthenticators(AMIdentity identity) throws AuthLoginException {
Set<Authenticator> authenticators = new HashSet<>();

Map<String, Set<String>> attributes = new HashMap<>();
attributes.put("uid", Collections.singleton(username));
AMIdentity user = accountProvider.searchUser(getAMIdentityRepository(getRequestOrg()), attributes);

try {
Set<String> authenticatorsMarshalled = user.getAttribute(userAttribute);
Set<String> 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;
Expand Down
Loading

0 comments on commit c7426c4

Please sign in to comment.