Skip to content

Commit

Permalink
feat: webauthn sign in
Browse files Browse the repository at this point in the history
  • Loading branch information
tamassoltesz committed Jan 31, 2025
1 parent 508e898 commit 0c682d8
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 38 deletions.
43 changes: 42 additions & 1 deletion src/main/java/io/supertokens/inmemorydb/Start.java
Original file line number Diff line number Diff line change
Expand Up @@ -3343,7 +3343,24 @@ public WebAuthNStoredCredential saveCredentials_Transaction(TenantIdentifier ten
@Override
public WebAuthNOptions loadOptionsById_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con,
String optionsId) throws StorageQueryException {
return null;
try {
Connection sqlCon = (Connection) con.getConnection();
return WebAuthNQueries.loadOptionsById_Transaction(this, sqlCon, tenantIdentifier, optionsId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
public WebAuthNStoredCredential loadCredentialById_Transaction(TenantIdentifier tenantIdentifier,
TransactionConnection con, String credentialId)
throws StorageQueryException {
try {
Connection sqlCon = (Connection) con.getConnection();
return WebAuthNQueries.loadCredentialById_Transaction(this, sqlCon, tenantIdentifier, credentialId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
Expand Down Expand Up @@ -3389,4 +3406,28 @@ public AuthRecipeUserInfo signUp_Transaction(TenantIdentifier tenantIdentifier,
throw new StorageQueryException(stle.actualException);
}
}

@Override
public AuthRecipeUserInfo getUserInfoByCredentialId_Transaction(TenantIdentifier tenantIdentifier,
TransactionConnection con, String credentialId)
throws StorageQueryException {
try {
Connection sqlCon = (Connection) con.getConnection();
return WebAuthNQueries.getUserInfoByCredentialId_Transaction(this, sqlCon, tenantIdentifier, credentialId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
public void updateCounter_Transaction(TenantIdentifier tenantIdentifier,
TransactionConnection con, String credentialId,
long counter) throws StorageQueryException {
try {
Connection sqlCon = (Connection) con.getConnection();
WebAuthNQueries.updateCounter_Transaction(this, sqlCon, tenantIdentifier, credentialId, counter);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,22 @@ public static WebAuthNOptions loadOptionsById(Start start, TenantIdentifier tena
}

public static WebAuthNStoredCredential loadCredential(Start start, TenantIdentifier tenantIdentifier, String credentialId)
throws StorageQueryException, StorageTransactionLogicException {
return start.startTransaction(con -> {
Connection sqlConnection = (Connection) con.getConnection();
try {
return loadCredentialById_Transaction(start, sqlConnection, tenantIdentifier, credentialId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
});
}

public static WebAuthNStoredCredential loadCredentialById_Transaction(Start start, Connection sqlConnection, TenantIdentifier tenantIdentifier, String credentialId)
throws SQLException, StorageQueryException {
String QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNCredentialsTable()
+ " WHERE app_id = ? AND id = ?";
return execute(start, QUERY, pst -> {
return execute(sqlConnection, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, credentialId);
}, result -> {
Expand Down Expand Up @@ -364,6 +376,70 @@ public static Collection<? extends LoginMethod> getUsersInfoUsingIdList_Transact
return Collections.emptyList();
}

public static AuthRecipeUserInfo getUserInfoByCredentialId_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String credentialId)
throws SQLException, StorageQueryException {

String QUERY = "SELECT webauthn.user_id as user_id, webauthn.email as email, webauthn.time_joined as time_joined, " +
"credentials.id as credential_id, email_verification.email_verified as email_verified, user_id_mapping.external_user_id as external_user_id," +
"all_users.tenant_id as tenant_id " +
"FROM " + getConfig(start).getWebAuthNUsersTable() + " as webauthn " +
"JOIN " + getConfig(start).getWebAuthNCredentialsTable() + " as credentials ON webauthn.user_id = credentials.user_id " +
"JOIN " + getConfig(start).getUsersTable() + " as all_users ON webauthn.app_id = all_users.app_id AND webauthn.user_id = all_users.user_id " +
"JOIN " + getConfig(start).getUserIdMappingTable() + " as user_id_mapping ON webauthn.user_id = user_id_mapping.supertokens_user_id " +
"JOIN " + getConfig(start).getEmailVerificationTable() + " as email_verification ON webauthn.app_id = email_verification.app_id AND user_id_mapping.external_user_id = email_verification.user_id OR user_id_mapping.supertokens_user_id = email_verification.user_id" +
"WHERE webauthn.app_id = ? AND credentials.id = ?";

return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, credentialId);
}, result -> {
if (result.next()) {
String userId = result.getString("user_id");
String email = result.getString("email");
long timeJoined = result.getLong("time_joined");
boolean emailVerified = result.getBoolean("email_verified");
String externalUserId = result.getString("external_user_id");
String tenantId = result.getString("tenant_id");
LoginMethod.WebAuthN webAuthNLM = new LoginMethod.WebAuthN(Collections.singletonList(credentialId));
LoginMethod loginMethod = new LoginMethod(userId, timeJoined, emailVerified, email, webAuthNLM, new String[]{tenantId});
if(externalUserId != null) {
loginMethod.setExternalUserId(externalUserId);
}
return AuthRecipeUserInfo.create(userId, false, loginMethod);
}
return null;
});
}

public static WebAuthNOptions loadOptionsById_Transaction(Start start, Connection sqlCon,
TenantIdentifier tenantIdentifier, String optionsId)
throws SQLException, StorageQueryException {
String QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable()
+ " WHERE app_id = ? AND id = ?";
return execute(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, optionsId);
}, result -> {
if(result.next()){
return WebAuthNOptionsRowMapper.getInstance().mapOrThrow(result); // we are expecting one or zero results
}
return null;
});
}

public static void updateCounter_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String credentialId, long counter)
throws SQLException, StorageQueryException {
String UPDATE = "UPDATE " + Config.getConfig(start).getWebAuthNCredentialsTable()
+ " SET counter = ?, updated_at = ? WHERE app_id = ? AND id = ?";

update(sqlCon, UPDATE, pst -> {
pst.setLong(1, counter);
pst.setLong(2, System.currentTimeMillis());
pst.setString(3, tenantIdentifier.getAppId());
pst.setString(4, credentialId);
});
}

private static class WebAuthnStoredCredentialRowMapper implements RowMapper<WebAuthNStoredCredential, ResultSet> {
private static final WebAuthnStoredCredentialRowMapper INSTANCE = new WebAuthnStoredCredentialRowMapper();

Expand Down
49 changes: 15 additions & 34 deletions src/main/java/io/supertokens/webauthn/WebAuthN.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,11 @@ public static JsonObject generateSignInOptions(TenantIdentifier tenantIdentifier
saveGeneratedOptions(tenantIdentifier, storage, challenge, timeout, relyingPartyId, relyingPartyName, origin,
null, optionsId);

JsonObject response = new JsonObject();
response.addProperty("webauthnGeneratedOptionsId", optionsId);
response.addProperty("rpId", relyingPartyId);
response.addProperty("challenge", Base64.getUrlEncoder().encodeToString(challenge.getValue()));
response.addProperty("timeout", timeout);
response.addProperty("userVerification", userVerification);

return response;
return WebauthMapper.mapSignInOptionsResponse(relyingPartyId, timeout, userVerification, optionsId, challenge);
}



@NotNull
private static Challenge getChallenge() {
Challenge challenge = new Challenge() {
Expand Down Expand Up @@ -231,35 +226,21 @@ public static WebAuthNSignInUpResult signIn(Storage storage, TenantIdentifier te
WebAuthNSQLStorage webAuthNStorage = (WebAuthNSQLStorage) storage;
webAuthNStorage.startTransaction(con -> {

while (true) {
try {

String recipeUserId = Utils.getUUID();
WebAuthNOptions generatedOptions = webAuthNStorage.loadOptionsById_Transaction(tenantIdentifier,
con,
webauthGeneratedOptionsId);

AuthRecipeUserInfo userInfo = webAuthNStorage.signUp_Transaction(tenantIdentifier, con, recipeUserId, generatedOptions.userEmail,
generatedOptions.relyingPartyId);
WebAuthNOptions generatedOptions = webAuthNStorage.loadOptionsById_Transaction(tenantIdentifier,
con,
webauthGeneratedOptionsId);

AuthRecipeUserInfo userInfo = webAuthNStorage.getUserInfoByCredentialId_Transaction(tenantIdentifier, con, credentialId);
WebAuthNStoredCredential credential = webAuthNStorage.loadCredentialById_Transaction(tenantIdentifier, con, credentialId);

RegistrationData verifiedRegistrationData = getRegistrationData(credentialsDataString,
generatedOptions);
WebAuthNStoredCredential credentialToSave = WebauthMapper.mapRegistrationDataToStoredCredential(
verifiedRegistrationData,
recipeUserId, credentialId, generatedOptions.userEmail, generatedOptions.relyingPartyId,
tenantIdentifier);
WebAuthNStoredCredential savedCredential = webAuthNStorage.saveCredentials_Transaction(
tenantIdentifier,
con, credentialToSave);

return new WebAuthNSignInUpResult(savedCredential, userInfo, generatedOptions);
} catch (DuplicateUserIdException duplicateUserIdException) {
//ignore and retry
} catch (Exception e) {
throw new RuntimeException(e);
}
try {
getRegistrationData(credentialsDataString, generatedOptions);
} catch (Exception e) {
throw new RuntimeException(e);
}

webAuthNStorage.updateCounter_Transaction(tenantIdentifier, con, credentialId, credential.counter + 1);
return new WebAuthNSignInUpResult(credential, userInfo, generatedOptions);
});
} catch (Exception e) {
throw new RuntimeException(e); // TODO! make it more specific
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/io/supertokens/webauthn/utils/WebauthMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.webauthn4j.converter.AttestedCredentialDataConverter;
import com.webauthn4j.converter.util.ObjectConverter;
import com.webauthn4j.data.*;
import com.webauthn4j.data.client.challenge.Challenge;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.webauthn.WebAuthNStoredCredential;
import io.supertokens.webauthn.WebauthNSaveCredentialResponse;
Expand Down Expand Up @@ -124,4 +125,15 @@ public static WebauthNSaveCredentialResponse mapStoredCredentialToResponse(WebAu
response.relyingPartyName = relyingPartyName;
return response;
}

public static JsonObject mapSignInOptionsResponse(String relyingPartyId, Long timeout, String userVerification,
String optionsId, Challenge challenge) {
JsonObject response = new JsonObject();
response.addProperty("webauthnGeneratedOptionsId", optionsId);
response.addProperty("rpId", relyingPartyId);
response.addProperty("challenge", Base64.getUrlEncoder().encodeToString(challenge.getValue()));
response.addProperty("timeout", timeout);
response.addProperty("userVerification", userVerification);
return response;
}
}
1 change: 1 addition & 0 deletions src/main/java/io/supertokens/webserver/Webserver.java
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ private void setupRoutes() {
addAPI(new CredentialsRegisterAPI(main));
addAPI(new SignUpWithCredentialRegisterAPI(main));
addAPI(new GetGeneratedOptionsAPI(main));
addAPI(new io.supertokens.webserver.api.webauthn.SignInAPI(main));

StandardContext context = tomcatReference.getContext();
Tomcat tomcat = tomcatReference.getTomcat();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO
new Gson().toJsonTree(options).getAsJsonObject().entrySet().forEach(entry -> {
result.add(entry.getKey(), entry.getValue());
});

super.sendJsonResponse(200, result, resp);
} catch (TenantOrAppNotFoundException | StorageQueryException e) {
throw new ServletException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
JsonObject result = new JsonObject();
result.addProperty("status", "OK");
result.add("user", new Gson().fromJson(new Gson().toJson(signInResult.userInfo), JsonObject.class));
result.add("options", new Gson().fromJson(new Gson().toJson(signInResult.options), JsonObject.class));

super.sendJsonResponse(200, result, resp);
} catch (TenantOrAppNotFoundException e) {
Expand Down

0 comments on commit 0c682d8

Please sign in to comment.