diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 1f87db8ec..cfc402645 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -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 @@ -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); + } + } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/WebAuthNQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/WebAuthNQueries.java index 5f2b17300..fb958d824 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/WebAuthNQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/WebAuthNQueries.java @@ -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 -> { @@ -364,6 +376,70 @@ public static Collection 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 { private static final WebAuthnStoredCredentialRowMapper INSTANCE = new WebAuthnStoredCredentialRowMapper(); diff --git a/src/main/java/io/supertokens/webauthn/WebAuthN.java b/src/main/java/io/supertokens/webauthn/WebAuthN.java index 2a752ed96..b66d14ed0 100644 --- a/src/main/java/io/supertokens/webauthn/WebAuthN.java +++ b/src/main/java/io/supertokens/webauthn/WebAuthN.java @@ -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() { @@ -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 diff --git a/src/main/java/io/supertokens/webauthn/utils/WebauthMapper.java b/src/main/java/io/supertokens/webauthn/utils/WebauthMapper.java index 4f12bb013..82dc3db8b 100644 --- a/src/main/java/io/supertokens/webauthn/utils/WebauthMapper.java +++ b/src/main/java/io/supertokens/webauthn/utils/WebauthMapper.java @@ -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; @@ -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; + } } \ No newline at end of file diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index 431cc6015..6cecc9671 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -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(); diff --git a/src/main/java/io/supertokens/webserver/api/webauthn/GetGeneratedOptionsAPI.java b/src/main/java/io/supertokens/webserver/api/webauthn/GetGeneratedOptionsAPI.java index d8633a6db..c340f93a9 100644 --- a/src/main/java/io/supertokens/webserver/api/webauthn/GetGeneratedOptionsAPI.java +++ b/src/main/java/io/supertokens/webserver/api/webauthn/GetGeneratedOptionsAPI.java @@ -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); diff --git a/src/main/java/io/supertokens/webserver/api/webauthn/SignInAPI.java b/src/main/java/io/supertokens/webserver/api/webauthn/SignInAPI.java index 747238fca..727a4b17a 100644 --- a/src/main/java/io/supertokens/webserver/api/webauthn/SignInAPI.java +++ b/src/main/java/io/supertokens/webserver/api/webauthn/SignInAPI.java @@ -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) {