From bd86375de4b9acf7ab5be78e19751079661c6f96 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 25 Sep 2024 16:10:57 +0530 Subject: [PATCH] fix: revoke APIs (#1041) * fix: revoke consent sessions * fix: revoke token * fix: revoke impl * fix: revoke session * fix: introspect impl after revoke * fix: revoke by client_id * fix: refresh token check in token exchange * fix: at hash * fix: sqlite impl * fix: client props whitelist * fix: status and query null check * fix: plugin interface update * fix: logout api * fix: ext * fix: accept consent * fix: accept consent * fix: introspect in token api * fix: keep fragment while updating query params * fix: count creds and pr comment * fix: oauth stats * fix: oauth cleanup cron task * fix: gid in refresh token * fix: inememory impl * feat: add initial payload fields to accept consent * fix: revoke cleanup * fix: stats * fix: client credentials basic * fix: authorization header * fix: authorizaion header in revoke * fix: missing table --------- Co-authored-by: Mihaly Lengyel --- .../java/io/supertokens/ee/EEFeatureFlag.java | 28 ++ src/main/java/io/supertokens/Main.java | 3 + .../CleanupOAuthRevokeList.java | 57 ++++ .../java/io/supertokens/inmemorydb/Start.java | 96 ++++++- .../inmemorydb/config/SQLiteConfig.java | 12 +- .../inmemorydb/queries/GeneralQueries.java | 19 +- .../inmemorydb/queries/OAuthQueries.java | 210 ++++++++++++++- .../supertokens/oauth/HttpRequestForOry.java | 10 +- src/main/java/io/supertokens/oauth/OAuth.java | 250 ++++++++++++++++-- .../java/io/supertokens/oauth/OAuthToken.java | 21 +- .../io/supertokens/oauth/Transformations.java | 69 ++++- .../io/supertokens/webserver/Webserver.java | 21 +- .../CreateUpdateOrGetOAuthClientAPI.java | 30 ++- .../OAuthAcceptAuthConsentRequestAPI.java | 31 +++ .../webserver/api/oauth/OAuthAuthAPI.java | 21 +- .../oauth/OAuthGetAuthConsentRequestAPI.java | 3 + .../oauth/OAuthGetAuthLoginRequestAPI.java | 3 + .../oauth/OAuthGetAuthLogoutRequestAPI.java | 3 + .../webserver/api/oauth/OAuthLogoutAPI.java | 73 +++++ .../webserver/api/oauth/OAuthProxyHelper.java | 6 +- .../webserver/api/oauth/OAuthTokenAPI.java | 74 +++++- .../api/oauth/OAuthTokenIntrospectAPI.java | 21 +- .../api/oauth/RemoveOAuthClientAPI.java | 1 + .../api/oauth/RevokeOAuthSessionAPI.java | 50 ++++ .../api/oauth/RevokeOAuthTokenAPI.java | 141 ++++++++++ .../api/oauth/RevokeOAuthTokensAPI.java | 70 +++++ .../test/oauth/api/OAuthAuthAPITest.java | 1 - .../test/oauth/api/OAuthClientsAPITest.java | 1 - 28 files changed, 1220 insertions(+), 105 deletions(-) create mode 100644 src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthSessionAPI.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index 4c959f6f8..e01b8ed26 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -24,6 +24,7 @@ import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.StorageUtils; import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; import io.supertokens.pluginInterface.dashboard.sqlStorage.DashboardSQLStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -32,6 +33,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.ThirdPartyConfig; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.utils.Utils; @@ -338,6 +340,28 @@ private JsonObject getAccountLinkingStats() throws StorageQueryException, Tenant return result; } + private JsonObject getOAuthStats() throws StorageQueryException, TenantOrAppNotFoundException { + JsonObject result = new JsonObject(); + + OAuthStorage oAuthStorage = StorageUtils.getOAuthStorage(StorageLayer.getStorage( + this.appIdentifier.getAsPublicTenantIdentifier(), main)); + + result.addProperty("totalNumberOfClients", oAuthStorage.countTotalNumberOfClientsForApp(appIdentifier)); + result.addProperty("numberOfClientCredentialsOnlyClients", oAuthStorage.countTotalNumberOfClientCredentialsOnlyClientsForApp(appIdentifier)); + result.addProperty("numberOfM2MTokensAlive", oAuthStorage.countTotalNumberOfM2MTokensAlive(appIdentifier)); + + long now = System.currentTimeMillis(); + JsonArray tokensCreatedArray = new JsonArray(); + for (int i = 1; i <= 31; i++) { + long timestamp = now - (i * 24 * 60 * 60 * 1000L); + int numberOfTokensCreated = oAuthStorage.countTotalNumberOfM2MTokensCreatedSince(this.appIdentifier, timestamp); + tokensCreatedArray.add(new JsonPrimitive(numberOfTokensCreated)); + } + result.add("numberOfM2MTokensCreated", tokensCreatedArray); + + return result; + } + private JsonArray getMAUs() throws StorageQueryException, TenantOrAppNotFoundException { JsonArray mauArr = new JsonArray(); long now = System.currentTimeMillis(); @@ -395,6 +419,10 @@ public JsonObject getPaidFeatureStats() throws StorageQueryException, TenantOrAp if (feature == EE_FEATURES.SECURITY) { usageStats.add(EE_FEATURES.SECURITY.toString(), new JsonObject()); } + + if (feature == EE_FEATURES.OAUTH) { + usageStats.add(EE_FEATURES.OAUTH.toString(), getOAuthStats()); + } } usageStats.add("maus", getMAUs()); diff --git a/src/main/java/io/supertokens/Main.java b/src/main/java/io/supertokens/Main.java index f6e9f29ea..1eef7e500 100644 --- a/src/main/java/io/supertokens/Main.java +++ b/src/main/java/io/supertokens/Main.java @@ -20,6 +20,7 @@ import io.supertokens.config.Config; import io.supertokens.config.CoreConfig; import io.supertokens.cronjobs.Cronjobs; +import io.supertokens.cronjobs.cleanupOAuthRevokeList.CleanupOAuthRevokeList; import io.supertokens.cronjobs.deleteExpiredAccessTokenSigningKeys.DeleteExpiredAccessTokenSigningKeys; import io.supertokens.cronjobs.deleteExpiredDashboardSessions.DeleteExpiredDashboardSessions; import io.supertokens.cronjobs.deleteExpiredEmailVerificationTokens.DeleteExpiredEmailVerificationTokens; @@ -256,6 +257,8 @@ private void init() throws IOException, StorageQueryException { // starts DeleteExpiredAccessTokenSigningKeys cronjob if the access token signing keys can change Cronjobs.addCronjob(this, DeleteExpiredAccessTokenSigningKeys.init(this, uniqueUserPoolIdsTenants)); + Cronjobs.addCronjob(this, CleanupOAuthRevokeList.init(this, uniqueUserPoolIdsTenants)); + // this is to ensure tenantInfos are in sync for the new cron job as well MultitenancyHelper.getInstance(this).refreshCronjobs(); diff --git a/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java b/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java new file mode 100644 index 000000000..a94d65d51 --- /dev/null +++ b/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java @@ -0,0 +1,57 @@ +package io.supertokens.cronjobs.cleanupOAuthRevokeList; + +import java.util.List; + +import io.supertokens.Main; +import io.supertokens.cronjobs.CronTask; +import io.supertokens.cronjobs.CronTaskTest; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.StorageUtils; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.oauth.OAuthStorage; +import io.supertokens.storageLayer.StorageLayer; + +public class CleanupOAuthRevokeList extends CronTask { + + public static final String RESOURCE_KEY = "io.supertokens.cronjobs.cleanupOAuthRevokeList" + + ".CleanupOAuthRevokeList"; + + private CleanupOAuthRevokeList(Main main, List> tenantsInfo) { + super("CleanupOAuthRevokeList", main, tenantsInfo, true); + } + + public static CleanupOAuthRevokeList init(Main main, List> tenantsInfo) { + return (CleanupOAuthRevokeList) main.getResourceDistributor() + .setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY, + new CleanupOAuthRevokeList(main, tenantsInfo)); + } + + @Override + protected void doTaskPerApp(AppIdentifier app) throws Exception { + Storage storage = StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main); + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.cleanUpExpiredAndRevokedTokens(app); + } + + @Override + public int getIntervalTimeSeconds() { + if (Main.isTesting) { + Integer interval = CronTaskTest.getInstance(main).getIntervalInSeconds(RESOURCE_KEY); + if (interval != null) { + return interval; + } + } + // Every 24 hours. + return 24 * 3600; + } + + @Override + public int getInitialWaitTimeSeconds() { + if (!Main.isTesting) { + return getIntervalTimeSeconds(); + } else { + return 0; + } + } +} diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 9cb99abbc..288e6593e 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -55,7 +55,6 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.multitenancy.sqlStorage.MultitenancySQLStorage; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; @@ -107,7 +106,6 @@ public class Start ActiveUsersSQLStorage, DashboardSQLStorage, AuthRecipeSQLStorage, OAuthSQLStorage { private static final Object appenderLock = new Object(); - private static final String APP_ID_KEY_NAME = "app_id"; private static final String ACCESS_TOKEN_SIGNING_KEY_NAME = "access_token_signing_key"; private static final String REFRESH_TOKEN_KEY_NAME = "refresh_token_key"; public static boolean isTesting = false; @@ -3011,7 +3009,7 @@ public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(A } @Override - public boolean doesClientIdExistForThisApp(AppIdentifier appIdentifier, String clientId) + public boolean doesClientIdExistForApp(AppIdentifier appIdentifier, String clientId) throws StorageQueryException { try { return OAuthQueries.isClientIdForAppId(this, clientId, appIdentifier); @@ -3021,19 +3019,11 @@ public boolean doesClientIdExistForThisApp(AppIdentifier appIdentifier, String c } @Override - public void addClientForApp(AppIdentifier appIdentifier, String clientId) - throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException { + public void addOrUpdateClientForApp(AppIdentifier appIdentifier, String clientId, boolean isClientCredentialsOnly) + throws StorageQueryException { try { - OAuthQueries.insertClientIdForAppId(this, clientId, appIdentifier); + OAuthQueries.insertClientIdForAppId(this, appIdentifier, clientId, isClientCredentialsOnly); } catch (SQLException e) { - - SQLiteConfig config = Config.getConfig(this); - String serverErrorMessage = e.getMessage(); - - if (isPrimaryKeyError(serverErrorMessage, config.getOAuthClientTable(), - new String[]{"app_id", "client_id"})) { - throw new OAuth2ClientAlreadyExistsForAppException(); - } throw new StorageQueryException(e); } } @@ -3055,4 +3045,82 @@ public List listClientsForApp(AppIdentifier appIdentifier) throws Storag throw new StorageQueryException(e); } } + + @Override + public void revoke(AppIdentifier appIdentifier, String targetType, String targetValue, long exp) + throws StorageQueryException { + try { + OAuthQueries.revoke(this, appIdentifier, targetType, targetValue, exp); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + + } + + @Override + public boolean isRevoked(AppIdentifier appIdentifier, String[] targetTypes, String[] targetValues, long issuedAt) + throws StorageQueryException { + try { + return OAuthQueries.isRevoked(this, appIdentifier, targetTypes, targetValues, issuedAt); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void addM2MToken(AppIdentifier appIdentifier, String clientId, long iat, long exp) + throws StorageQueryException { + try { + OAuthQueries.addM2MToken(this, appIdentifier, clientId, iat, exp); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void cleanUpExpiredAndRevokedTokens(AppIdentifier appIdentifier) throws StorageQueryException { + try { + OAuthQueries.cleanUpExpiredAndRevokedTokens(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfClientCredentialsOnlyClientsForApp(AppIdentifier appIdentifier) + throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfClientsForApp(this, appIdentifier, true); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfClientsForApp(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfClientsForApp(this, appIdentifier, false); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfM2MTokensAlive(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfM2MTokensAlive(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfM2MTokensCreatedSince(AppIdentifier appIdentifier, long since) + throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfM2MTokensCreatedSince(this, appIdentifier, since); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } } diff --git a/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java b/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java index bc969dc6f..c14646456 100644 --- a/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java +++ b/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java @@ -165,5 +165,15 @@ public String getDashboardSessionsTable() { return "dashboard_user_sessions"; } - public String getOAuthClientTable(){ return "oauth_clients"; } + public String getOAuthClientsTable() { + return "oauth_clients"; + } + + public String getOAuthRevokeTable() { + return "oauth_revoke"; + } + + public String getOAuthM2MTokensTable() { + return "oauth_m2m_tokens"; + } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java index 665511467..13d4ee092 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java @@ -423,10 +423,27 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc update(start, TOTPQueries.getQueryToCreateUsedCodesExpiryTimeIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getOAuthClientTable())) { + if (!doesTableExists(start, Config.getConfig(start).getOAuthClientsTable())) { getInstance(main).addState(CREATING_NEW_TABLE, null); update(start, OAuthQueries.getQueryToCreateOAuthClientTable(start), NO_OP_SETTER); } + + if (!doesTableExists(start, Config.getConfig(start).getOAuthRevokeTable())) { + getInstance(main).addState(CREATING_NEW_TABLE, null); + update(start, OAuthQueries.getQueryToCreateOAuthRevokeTable(start), NO_OP_SETTER); + + // index + update(start, OAuthQueries.getQueryToCreateOAuthRevokeTimestampIndex(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, Config.getConfig(start).getOAuthM2MTokensTable())) { + getInstance(main).addState(CREATING_NEW_TABLE, null); + update(start, OAuthQueries.getQueryToCreateOAuthM2MTokensTable(start), NO_OP_SETTER); + + // index + update(start, OAuthQueries.getQueryToCreateOAuthM2MTokenIatIndex(start), NO_OP_SETTER); + update(start, OAuthQueries.getQueryToCreateOAuthM2MTokenExpIndex(start), NO_OP_SETTER); + } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java index 806fc9c6d..5a27ee8d5 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java @@ -32,19 +32,69 @@ public class OAuthQueries { public static String getQueryToCreateOAuthClientTable(Start start) { - String oAuth2ClientTable = Config.getConfig(start).getOAuthClientTable(); + String oAuth2ClientTable = Config.getConfig(start).getOAuthClientsTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + oAuth2ClientTable + " (" + "app_id VARCHAR(64)," + "client_id VARCHAR(128) NOT NULL," + + "is_client_credentials_only BOOLEAN NOT NULL," + " PRIMARY KEY (app_id, client_id)," + " FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE);"; // @formatter:on } + public static String getQueryToCreateOAuthRevokeTable(Start start) { + String oAuth2RevokeTable = Config.getConfig(start).getOAuthRevokeTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + oAuth2RevokeTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "target_type VARCHAR(16) NOT NULL," + + "target_value VARCHAR(128) NOT NULL," + + "timestamp BIGINT NOT NULL, " + + "exp BIGINT NOT NULL," + + "PRIMARY KEY (app_id, target_type, target_value)," + + "FOREIGN KEY(app_id) " + + " REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + public static String getQueryToCreateOAuthRevokeTimestampIndex(Start start) { + String oAuth2RevokeTable = Config.getConfig(start).getOAuthRevokeTable(); + return "CREATE INDEX IF NOT EXISTS oauth_revoke_timestamp_index ON " + + oAuth2RevokeTable + "(timestamp DESC, app_id DESC);"; + } + + public static String getQueryToCreateOAuthM2MTokensTable(Start start) { + String oAuth2M2MTokensTable = Config.getConfig(start).getOAuthM2MTokensTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + oAuth2M2MTokensTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "client_id VARCHAR(128) NOT NULL," + + "iat BIGINT NOT NULL," + + "exp BIGINT NOT NULL," + + "PRIMARY KEY (app_id, client_id, iat)," + + "FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + public static String getQueryToCreateOAuthM2MTokenIatIndex(Start start) { + String oAuth2M2MTokensTable = Config.getConfig(start).getOAuthM2MTokensTable(); + return "CREATE INDEX IF NOT EXISTS oauth_m2m_token_iat_index ON " + + oAuth2M2MTokensTable + "(iat DESC, app_id DESC);"; + } + + public static String getQueryToCreateOAuthM2MTokenExpIndex(Start start) { + String oAuth2M2MTokensTable = Config.getConfig(start).getOAuthM2MTokensTable(); + return "CREATE INDEX IF NOT EXISTS oauth_m2m_token_exp_index ON " + + oAuth2M2MTokensTable + "(exp DESC, app_id DESC);"; + } + public static boolean isClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthClientTable() + + String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthClientsTable() + " WHERE client_id = ? AND app_id = ?"; return execute(start, QUERY, pst -> { @@ -55,7 +105,7 @@ public static boolean isClientIdForAppId(Start start, String clientId, AppIdenti public static List listClientsForApp(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - String QUERY = "SELECT client_id FROM " + Config.getConfig(start).getOAuthClientTable() + + String QUERY = "SELECT client_id FROM " + Config.getConfig(start).getOAuthClientsTable() + " WHERE app_id = ?"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -68,19 +118,23 @@ public static List listClientsForApp(Start start, AppIdentifier appIdent }); } - public static void insertClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier) + public static void insertClientIdForAppId(Start start, AppIdentifier appIdentifier, String clientId, + boolean isClientCredentialsOnly) throws SQLException, StorageQueryException { - String INSERT = "INSERT INTO " + Config.getConfig(start).getOAuthClientTable() - + "(app_id, client_id) VALUES(?, ?)"; + String INSERT = "INSERT INTO " + Config.getConfig(start).getOAuthClientsTable() + + "(app_id, client_id, is_client_credentials_only) VALUES(?, ?, ?) " + + "ON CONFLICT (app_id, client_id) DO UPDATE SET is_client_credentials_only = ?"; update(start, INSERT, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, clientId); + pst.setBoolean(3, isClientCredentialsOnly); + pst.setBoolean(4, isClientCredentialsOnly); }); } public static boolean deleteClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthClientTable() + String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthClientsTable() + " WHERE app_id = ? AND client_id = ?"; int numberOfRow = update(start, DELETE, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -89,4 +143,146 @@ public static boolean deleteClientIdForAppId(Start start, String clientId, AppId return numberOfRow > 0; } + public static void revoke(Start start, AppIdentifier appIdentifier, String targetType, String targetValue, long exp) + throws SQLException, StorageQueryException { + String INSERT = "INSERT INTO " + Config.getConfig(start).getOAuthRevokeTable() + + "(app_id, target_type, target_value, timestamp, exp) VALUES (?, ?, ?, ?, ?) " + + "ON CONFLICT (app_id, target_type, target_value) DO UPDATE SET timestamp = ?, exp = ?"; + + long currentTime = System.currentTimeMillis() / 1000; + update(start, INSERT, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, targetType); + pst.setString(3, targetValue); + pst.setLong(4, currentTime); + pst.setLong(5, exp); + pst.setLong(6, currentTime); + pst.setLong(7, exp); + }); + } + + public static boolean isRevoked(Start start, AppIdentifier appIdentifier, String[] targetTypes, String[] targetValues, long issuedAt) + throws SQLException, StorageQueryException { + String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthRevokeTable() + + " WHERE app_id = ? AND timestamp > ? AND ("; + + for (int i = 0; i < targetTypes.length; i++) { + QUERY += "(target_type = ? AND target_value = ?)"; + + if (i < targetTypes.length - 1) { + QUERY += " OR "; + } + } + + QUERY += ")"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, issuedAt); + + int index = 3; + for (int i = 0; i < targetTypes.length; i++) { + pst.setString(index, targetTypes[i]); + index++; + pst.setString(index, targetValues[i]); + index++; + } + }, ResultSet::next); + } + + public static int countTotalNumberOfClientsForApp(Start start, AppIdentifier appIdentifier, + boolean filterByClientCredentialsOnly) throws SQLException, StorageQueryException { + if (filterByClientCredentialsOnly) { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE app_id = ? AND is_client_credentials_only = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setBoolean(2, true); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } else { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE app_id = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + } + + public static int countTotalNumberOfM2MTokensAlive(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " WHERE app_id = ? AND exp > ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, System.currentTimeMillis()/1000); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + + public static int countTotalNumberOfM2MTokensCreatedSince(Start start, AppIdentifier appIdentifier, long since) + throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " WHERE app_id = ? AND iat >= ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, since / 1000); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + + public static void addM2MToken(Start start, AppIdentifier appIdentifier, String clientId, long iat, long exp) + throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getOAuthM2MTokensTable() + + " (app_id, client_id, iat, exp) VALUES (?, ?, ?, ?)"; + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, clientId); + pst.setLong(3, iat); + pst.setLong(4, exp); + }); + } + + public static void cleanUpExpiredAndRevokedTokens(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { + { + // delete expired M2M tokens + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " WHERE app_id = ? AND exp < ?"; + + long timestamp = System.currentTimeMillis() / 1000 - 3600 * 24 * 31; // expired 31 days ago + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, timestamp); + }); + } + + { + // delete expired revoked tokens + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthRevokeTable() + + " WHERE app_id = ? AND exp < ?"; + + long timestamp = System.currentTimeMillis() / 1000 - 3600 * 24 * 31; // expired 31 days ago + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, timestamp); + }); + } + } } diff --git a/src/main/java/io/supertokens/oauth/HttpRequestForOry.java b/src/main/java/io/supertokens/oauth/HttpRequestForOry.java index 62f5945e8..56e92bf55 100644 --- a/src/main/java/io/supertokens/oauth/HttpRequestForOry.java +++ b/src/main/java/io/supertokens/oauth/HttpRequestForOry.java @@ -127,9 +127,15 @@ public static Response doJsonPut(String url, Map queryParams, Ma } } - public static Response doJsonDelete(String url, Map headers, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { + public static Response doJsonDelete(String url, Map headers, Map queryParams, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { try { - URL obj = new URL(url); + if (queryParams == null) { + queryParams = new HashMap<>(); + } + + URL obj = new URL(url + "?" + queryParams.entrySet().stream() + .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&"))); HttpURLConnection con = (HttpURLConnection) obj.openConnection(); con.setRequestMethod("DELETE"); con.setConnectTimeout(CONNECTION_TIMEOUT); diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index ba323cca2..0c73d6a33 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -37,12 +37,13 @@ import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.oauth.OAuthStorage; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.session.jwt.JWT.JWTException; import io.supertokens.utils.Utils; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.*; @@ -72,7 +73,7 @@ public static HttpRequestForOry.Response doOAuthProxyGET(Main main, AppIdentifie } if (clientIdToCheck != null) { - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -113,7 +114,7 @@ public static HttpRequestForOry.Response doOAuthProxyFormPOST(Main main, AppIden } if (clientIdToCheck != null) { - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -154,7 +155,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPOST(Main main, AppIden } if (clientIdToCheck != null) { - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -196,7 +197,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPUT(Main main, AppIdent } if (clientIdToCheck != null) { - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -228,7 +229,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPUT(Main main, AppIdent return response; } - public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -237,7 +238,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppId } if (clientIdToCheck != null) { - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -254,7 +255,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppId } String fullUrl = baseURL + path; - HttpRequestForOry.Response response = HttpRequestForOry.doJsonDelete(fullUrl, headers, jsonInput); + HttpRequestForOry.Response response = HttpRequestForOry.doJsonDelete(fullUrl, queryParams, headers, jsonInput); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); @@ -283,31 +284,87 @@ private static void checkNonSuccessResponse(HttpRequestForOry.Response response) } } + public static String transformTokensInAuthRedirect(Main main, AppIdentifier appIdentifier, Storage storage, String url, String iss, JsonObject accessTokenUpdate, JsonObject idTokenUpdate, boolean useDynamicKey) { + if (url.indexOf('#') == -1) { + return url; + } + + try { + // Extract the part after '#' + String fragment = url.substring(url.indexOf('#') + 1); + + // Parse the fragment as query parameters + // Create a JsonObject from the params + JsonObject jsonBody = new JsonObject(); + for (String param : fragment.split("&")) { + String[] keyValue = param.split("=", 2); + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = java.net.URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8.toString()); + jsonBody.addProperty(key, value); + } + } + + // Transform the tokens + JsonObject transformedJson = transformTokens(main, appIdentifier, storage, jsonBody, iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); + + // Reconstruct the query params + StringBuilder newFragment = new StringBuilder(); + for (Map.Entry entry : transformedJson.entrySet()) { + if (newFragment.length() > 0) { + newFragment.append("&"); + } + String encodedValue = java.net.URLEncoder.encode(entry.getValue().getAsString(), StandardCharsets.UTF_8.toString()); + newFragment.append(entry.getKey()).append("=").append(encodedValue); + } + + // Reconstruct the URL + String baseUrl = url.substring(0, url.indexOf('#')); + return baseUrl + "#" + newFragment.toString(); + } catch (Exception e) { + // If any exception occurs, return the original URL + return url; + } + } + public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject jsonBody, String iss, JsonObject accessTokenUpdate, JsonObject idTokenUpdate, boolean useDynamicKey) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { + String atHash = null; + + if (jsonBody.has("refresh_token")) { + String refreshToken = jsonBody.get("refresh_token").getAsString(); + refreshToken = refreshToken.replace("ory_rt_", "st_rt_"); + jsonBody.addProperty("refresh_token", refreshToken); + } + if (jsonBody.has("access_token")) { String accessToken = jsonBody.get("access_token").getAsString(); - accessToken = OAuthToken.reSignToken(appIdentifier, main, accessToken, iss, accessTokenUpdate, OAuthToken.TokenType.ACCESS_TOKEN, useDynamicKey, 0); + accessToken = OAuthToken.reSignToken(appIdentifier, main, accessToken, iss, accessTokenUpdate, null, OAuthToken.TokenType.ACCESS_TOKEN, useDynamicKey, 0); jsonBody.addProperty("access_token", accessToken); + + // Compute at_hash as per OAuth 2.0 standard + // 1. Take the access token + // 2. Hash it with SHA-256 + // 3. Take the left-most half of the hash + // 4. Base64url encode it + byte[] accessTokenBytes = accessToken.getBytes(StandardCharsets.UTF_8); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(accessTokenBytes); + byte[] halfHash = Arrays.copyOf(hash, hash.length / 2); + atHash = Base64.getUrlEncoder().withoutPadding().encodeToString(halfHash); } if (jsonBody.has("id_token")) { String idToken = jsonBody.get("id_token").getAsString(); - idToken = OAuthToken.reSignToken(appIdentifier, main, idToken, iss, idTokenUpdate, OAuthToken.TokenType.ID_TOKEN, useDynamicKey, 0); + idToken = OAuthToken.reSignToken(appIdentifier, main, idToken, iss, idTokenUpdate, atHash, OAuthToken.TokenType.ID_TOKEN, useDynamicKey, 0); jsonBody.addProperty("id_token", idToken); } - if (jsonBody.has("refresh_token")) { - String refreshToken = jsonBody.get("refresh_token").getAsString(); - refreshToken = refreshToken.replace("ory_rt_", "st_rt_"); - jsonBody.addProperty("refresh_token", refreshToken); - } - return jsonBody; } - public static void addClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException { + public static void addOrUpdateClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId, boolean isClientCredentialsOnly) throws StorageQueryException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - oauthStorage.addClientForApp(appIdentifier, clientId); + oauthStorage.addOrUpdateClientForApp(appIdentifier, clientId, isClientCredentialsOnly); } public static void removeClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException { @@ -365,16 +422,87 @@ private static JsonElement convertSnakeCaseToCamelCaseRecursively(JsonElement js } + public static void verifyAndUpdateIntrospectRefreshTokenPayload(Main main, AppIdentifier appIdentifier, + Storage storage, JsonObject payload, String refreshToken) throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException { + + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + + if (!payload.get("active").getAsBoolean()) { + return; // refresh token is not active + } + + Transformations.transformExt(payload); + payload.remove("ext"); + + boolean isValid = !isTokenRevokedBasedOnPayload(oauthStorage, appIdentifier, payload); + + if (!isValid) { + payload.entrySet().clear(); + payload.addProperty("active", false); + + // // ideally we want to revoke the refresh token in hydra, but we can't since we don't have the client secret here + // refreshToken = refreshToken.replace("st_rt_", "ory_rt_"); + // Map formFields = new HashMap<>(); + // formFields.put("token", refreshToken); + + // try { + // doOAuthProxyFormPOST( + // main, appIdentifier, oauthStorage, + // clientId, // clientIdToCheck + // "/oauth2/revoke", // path + // false, // proxyToAdmin + // false, // camelToSnakeCaseConversion + // formFields, + // new HashMap<>()); + // } catch (OAuthAPIException | OAuthClientNotFoundException e) { + // // ignore + // } + } + } + + private static boolean isTokenRevokedBasedOnPayload(OAuthStorage oauthStorage, AppIdentifier appIdentifier, JsonObject payload) throws StorageQueryException { + long issuedAt = payload.get("iat").getAsLong(); + List targetTypes = new ArrayList<>(); + List targetValues = new ArrayList<>(); + + targetTypes.add("client_id"); + targetValues.add(payload.get("client_id").getAsString()); + + if (payload.has("jti")) { + targetTypes.add("jti"); + targetValues.add(payload.get("jti").getAsString()); + } + + if (payload.has("gid")) { + targetTypes.add("gid"); + targetValues.add(payload.get("gid").getAsString()); + } + + if (payload.has("sessionHandle")) { + targetTypes.add("session_handle"); + targetValues.add(payload.get("sessionHandle").getAsString()); + } + + return oauthStorage.isRevoked(appIdentifier, targetTypes.toArray(new String[0]), targetValues.toArray(new String[0]), issuedAt); + } + public static JsonObject introspectAccessToken(Main main, AppIdentifier appIdentifier, Storage storage, String token) throws StorageQueryException, StorageTransactionLogicException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException { try { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); JsonObject payload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, token); + if (payload.has("stt") && payload.get("stt").getAsInt() == OAuthToken.TokenType.ACCESS_TOKEN.getValue()) { - payload.addProperty("active", true); - payload.addProperty("token_type", "Bearer"); - payload.addProperty("token_use", "access_token"); - return payload; + boolean isValid = !isTokenRevokedBasedOnPayload(oauthStorage, appIdentifier, payload); + + if (isValid) { + payload.addProperty("active", true); + payload.addProperty("token_type", "Bearer"); + payload.addProperty("token_use", "access_token"); + + return payload; + } } // else fallback to active: false @@ -386,4 +514,82 @@ public static JsonObject introspectAccessToken(Main main, AppIdentifier appIdent result.addProperty("active", false); return result; } + + public static void revokeTokensForClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException { + long exp = System.currentTimeMillis() / 1000 + 3600 * 24 * 183; // 6 month from now + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.revoke(appIdentifier, "client_id", clientId, exp); + } + + public static void revokeRefreshToken(Main main, AppIdentifier appIdentifier, Storage storage, String gid, long exp) throws StorageQueryException, NoSuchAlgorithmException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.revoke(appIdentifier, "gid", gid, exp); + } + + public static void revokeAccessToken(Main main, AppIdentifier appIdentifier, + Storage storage, String token) throws StorageQueryException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + try { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + JsonObject payload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, token); + + long exp = payload.get("exp").getAsLong(); + + if (payload.has("stt") && payload.get("stt").getAsInt() == OAuthToken.TokenType.ACCESS_TOKEN.getValue()) { + String jti = payload.get("jti").getAsString(); + oauthStorage.revoke(appIdentifier, "jti", jti, exp); + } + + } catch (TryRefreshTokenException e) { + // the token is already invalid or revoked, so ignore + } + } + + public static void revokeSessionHandle(Main main, AppIdentifier appIdentifier, Storage storage, + String sessionHandle) throws StorageQueryException { + long exp = System.currentTimeMillis() / 1000 + 3600 * 24 * 183; // 6 month from now + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.revoke(appIdentifier, "session_handle", sessionHandle, exp); + } + + public static void verifyIdTokenHintClientIdAndUpdateQueryParamsForLogout(Main main, AppIdentifier appIdentifier, Storage storage, + Map queryParams) throws StorageQueryException, OAuthAPIException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + + String idTokenHint = queryParams.get("idTokenHint"); + String clientId = queryParams.get("clientId"); + + JsonObject idTokenPayload = null; + if (idTokenHint != null) { + queryParams.remove("idTokenHint"); + + try { + idTokenPayload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, idTokenHint); + } catch (TryRefreshTokenException e) { + // invalid id token + throw new OAuthAPIException("invalid_request", "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.", 400); + } + } + + if (idTokenPayload != null) { + if (!idTokenPayload.has("stt") || idTokenPayload.get("stt").getAsInt() != OAuthToken.TokenType.ID_TOKEN.getValue()) { + // Invalid id token + throw new OAuthAPIException("invalid_request", "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.", 400); + } + + String clientIdInIdTokenPayload = idTokenPayload.get("aud").getAsString(); + + if (clientId != null) { + if (!clientId.equals(clientIdInIdTokenPayload)) { + throw new OAuthAPIException("invalid_request", "The client_id in the id_token_hint does not match the client_id in the request.", 400); + } + } + + queryParams.put("clientId", clientIdInIdTokenPayload); + } + } + + public static void addM2MToken(Main main, AppIdentifier appIdentifier, Storage storage, String accessToken) throws StorageQueryException, TenantOrAppNotFoundException, TryRefreshTokenException, UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + JsonObject payload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, accessToken); + oauthStorage.addM2MToken(appIdentifier, payload.get("client_id").getAsString(), payload.get("iat").getAsLong(), payload.get("exp").getAsLong()); + } } diff --git a/src/main/java/io/supertokens/oauth/OAuthToken.java b/src/main/java/io/supertokens/oauth/OAuthToken.java index e04143c71..0d92b0b96 100644 --- a/src/main/java/io/supertokens/oauth/OAuthToken.java +++ b/src/main/java/io/supertokens/oauth/OAuthToken.java @@ -49,7 +49,7 @@ public int getValue() { private static Set NON_OVERRIDABLE_TOKEN_PROPS = Set.of( "kid", "typ", "alg", "aud", "iss", "iat", "exp", "nbf", "jti", "ext", - "sid", "rat", "at_hash", "rt_hash", + "sid", "rat", "at_hash", "gid", "client_id", "scp", "sub", "stt" ); @@ -95,18 +95,35 @@ public static JsonObject getPayloadFromJWTToken(AppIdentifier appIdentifier, return jwtInfo.payload; } - public static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, JsonObject payloadUpdate, TokenType tokenType, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, + public static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, JsonObject payloadUpdate, String atHash, TokenType tokenType, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException { JsonObject payload = JWT.getPayloadWithoutVerifying(token).payload; payload.addProperty("iss", iss); payload.addProperty("stt", tokenType.getValue()); + if (atHash != null) { + payload.addProperty("at_hash", atHash); + } if (tokenType == TokenType.ACCESS_TOKEN) { // we need to move rsub, tId and sessionHandle from ext to root Transformations.transformExt(payload); } + // This should only happen in the authorization code flow during the token exchange. (enforced on the api level) + // Other flows (including later calls using the refresh token) will have the payloadUpdate defined. + if (payloadUpdate == null) { + if (tokenType == TokenType.ACCESS_TOKEN) { + if (payload.has("ext") && payload.get("ext").isJsonObject()) { + payloadUpdate = payload.getAsJsonObject("ext").getAsJsonObject("initialPayload"); + payload.remove("ext"); + } + } else { + payloadUpdate = payload.getAsJsonObject("initialPayload"); + payload.remove("initialPayload"); + } + } + if (payloadUpdate != null) { for (Map.Entry entry : payloadUpdate.entrySet()) { if (!NON_OVERRIDABLE_TOKEN_PROPS.contains(entry.getKey())) { diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index 589a553cf..aa842470b 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -22,7 +22,37 @@ import io.supertokens.utils.Utils; public class Transformations { - private static Set EXT_PROPS = Set.of("rsub", "tId", "sessionHandle"); + private static Set EXT_PROPS = Set.of("iss", "rsub", "tId", "sessionHandle", "gid"); + + private static Set CLIENT_PROPS = Set.of( + "clientId", + "clientSecret", + "clientName", + "scope", + "redirectUris", + "postLogoutRedirectUris", + "authorizationCodeGrantAccessTokenLifespan", + "authorizationCodeGrantIdTokenLifespan", + "authorizationCodeGrantRefreshTokenLifespan", + "clientCredentialsGrantAccessTokenLifespan", + "implicitGrantAccessTokenLifespan", + "implicitGrantIdTokenLifespan", + "refreshTokenGrantAccessTokenLifespan", + "refreshTokenGrantIdTokenLifespan", + "refreshTokenGrantRefreshTokenLifespan", + "tokenEndpointAuthMethod", + "clientUri", + "allowedCorsOrigins", + "audience", + "grantTypes", + "responseTypes", + "logoUri", + "policyUri", + "tosUri", + "createdAt", + "updatedAt", + "metadata" + ); public static Map transformRequestHeadersForHydra(Map requestHeaders) { if (requestHeaders == null) { @@ -41,18 +71,19 @@ private static String transformQueryParamsInURLFromHydra(String redirectTo) { try { URL url = new URL(redirectTo); String query = url.getQuery(); - String[] queryParams = query.split("&"); - StringBuilder updatedQuery = new StringBuilder(); - for (String param : queryParams) { - String[] keyValue = param.split("="); - if (keyValue.length > 1 && keyValue[1].startsWith("ory_")) { - updatedQuery.append(keyValue[0]).append("=").append(keyValue[1].replaceFirst("ory_", "st_")).append("&"); - } else { - updatedQuery.append(param).append("&"); + if (query != null) { + String[] queryParams = query.split("&"); + StringBuilder updatedQuery = new StringBuilder(); + for (String param : queryParams) { + String[] keyValue = param.split("="); + if (keyValue.length > 1 && keyValue[1].startsWith("ory_")) { + updatedQuery.append(keyValue[0]).append("=").append(keyValue[1].replaceFirst("ory_", "st_")).append("&"); + } else { + updatedQuery.append(param).append("&"); + } } + redirectTo = redirectTo.replace("?" + query, "?" + updatedQuery.toString().trim()); } - redirectTo = url.getProtocol() + "://" + url.getHost() + ":" + url.getPort() + url.getPath() + "?" - + updatedQuery.toString().trim(); } catch (MalformedURLException e) { throw new IllegalStateException(e); } @@ -216,10 +247,20 @@ public static void transformExt(JsonObject payload) { ext.remove(prop); } } - - if (ext.entrySet().size() == 0) { - payload.remove("ext"); + } + } + + public static void applyClientPropsWhiteList(JsonObject payload) { + List propsToRemove = new ArrayList<>(); + + for (Map.Entry entry : payload.entrySet()) { + if (!CLIENT_PROPS.contains(entry.getKey())) { + propsToRemove.add(entry.getKey()); } } + + for (String prop : propsToRemove) { + payload.remove(prop); + } } } diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index da4c134aa..f283ca8b2 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -39,21 +39,7 @@ import io.supertokens.webserver.api.multitenancy.*; import io.supertokens.webserver.api.multitenancy.thirdparty.CreateOrUpdateThirdPartyConfigAPI; import io.supertokens.webserver.api.multitenancy.thirdparty.RemoveThirdPartyConfigAPI; -import io.supertokens.webserver.api.oauth.OAuthAuthAPI; -import io.supertokens.webserver.api.oauth.OAuthClientListAPI; -import io.supertokens.webserver.api.oauth.OAuthGetAuthConsentRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthGetAuthLoginRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthGetAuthLogoutRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthRejectAuthConsentRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthRejectAuthLoginRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthRejectAuthLogoutRequestAPI; -import io.supertokens.webserver.api.oauth.CreateUpdateOrGetOAuthClientAPI; -import io.supertokens.webserver.api.oauth.OAuthAcceptAuthConsentRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthAcceptAuthLoginRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthAcceptAuthLogoutRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthTokenAPI; -import io.supertokens.webserver.api.oauth.OAuthTokenIntrospectAPI; -import io.supertokens.webserver.api.oauth.RemoveOAuthClientAPI; +import io.supertokens.webserver.api.oauth.*; import io.supertokens.webserver.api.passwordless.*; import io.supertokens.webserver.api.session.*; import io.supertokens.webserver.api.thirdparty.GetUsersByEmailAPI; @@ -299,6 +285,11 @@ private void setupRoutes() { addAPI(new OAuthRejectAuthLogoutRequestAPI(main)); addAPI(new OAuthTokenIntrospectAPI(main)); + addAPI(new RevokeOAuthTokenAPI(main)); + addAPI(new RevokeOAuthTokensAPI(main)); + addAPI(new RevokeOAuthSessionAPI(main)); + addAPI(new OAuthLogoutAPI(main)); + StandardContext context = tomcatReference.getContext(); Tomcat tomcat = tomcatReference.getTomcat(); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index cfc811e8f..7203f0949 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.UUID; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -28,6 +29,7 @@ import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.Transformations; import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.oauth.exceptions.OAuthClientNotFoundException; import io.supertokens.pluginInterface.RECIPE_ID; @@ -36,7 +38,6 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -70,6 +71,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO new HashMap<>() ); if (response != null) { + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { @@ -85,6 +88,12 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I input.addProperty("accessTokenStrategy", "jwt"); input.addProperty("skipConsent", true); input.addProperty("subjectType", "public"); + input.addProperty("clientId", "stcl_" + UUID.randomUUID()); + + boolean isClientCredentialsOnly = input.has("grantTypes") && + input.get("grantTypes").isJsonArray() && + input.get("grantTypes").getAsJsonArray().size() == 1 && + input.get("grantTypes").getAsJsonArray().get(0).getAsString().equals("client_credentials"); try { AppIdentifier appIdentifier = getAppIdentifier(req); @@ -107,12 +116,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String clientId = response.jsonResponse.getAsJsonObject().get("clientId").getAsString(); try { - OAuth.addClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); + OAuth.addOrUpdateClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId, isClientCredentialsOnly); } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); - } catch (OAuth2ClientAlreadyExistsForAppException e) { - // ignore } + + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { @@ -124,6 +134,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { JsonObject input = InputParser.parseJsonObjectOrThrowError(req); String clientId = InputParser.parseStringOrThrowError(input, "clientId", false); + boolean isClientCredentialsOnly = input.has("grantTypes") && + input.get("grantTypes").isJsonArray() && + input.get("grantTypes").getAsJsonArray().size() == 1 && + input.get("grantTypes").getAsJsonArray().get(0).getAsString().equals("client_credentials"); // Apply existing client config on top of input try { @@ -165,6 +179,14 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO ); if (response != null) { + try { + OAuth.addOrUpdateClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId, isClientCredentialsOnly); + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java index 3df0236d0..f9d28c9fc 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.util.HashMap; +import java.util.UUID; import com.google.gson.JsonObject; @@ -30,6 +31,36 @@ public String getPath() { @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String iss = InputParser.parseStringOrThrowError(input, "iss", false); + String tId = InputParser.parseStringOrThrowError(input, "tId", false); + String rsub = InputParser.parseStringOrThrowError(input, "rsub", false); + String sessionHandle = InputParser.parseStringOrThrowError(input, "sessionHandle", false); + JsonObject initialAccessTokenPayload = InputParser.parseJsonObjectOrThrowError(input, "initialAccessTokenPayload", false); + JsonObject initialIdTokenPayload = InputParser.parseJsonObjectOrThrowError(input, "initialIdTokenPayload", false); + + JsonObject accessToken = new JsonObject(); + accessToken.addProperty("iss", iss); + accessToken.addProperty("tId", tId); + accessToken.addProperty("rsub", rsub); + accessToken.addProperty("sessionHandle", sessionHandle); + accessToken.add("initialPayload", initialAccessTokenPayload); + + JsonObject idToken = new JsonObject(); + idToken.add("initialPayload", initialIdTokenPayload); + accessToken.addProperty("gid", UUID.randomUUID().toString()); + + // remove the above from input + input.remove("iss"); + input.remove("tId"); + input.remove("rsub"); + input.remove("sessionHandle"); + input.remove("initialAccessTokenPayload"); + input.remove("initialIdTokenPayload"); + + JsonObject session = new JsonObject(); + session.add("access_token", accessToken); + session.add("id_token", idToken); + input.add("session", session); try { HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java index c7b604801..7ff44dcfc 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -23,7 +23,10 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; @@ -53,6 +56,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject params = InputParser.parseJsonObjectOrThrowError(input, "params", false); String cookies = InputParser.parseStringOrThrowError(input, "cookies", true); + // These optional stuff will be used in case of implicit flow + JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "access_token", true); + JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "id_token", true); + String iss = InputParser.parseStringOrThrowError(input, "iss", true); + Boolean useStaticKeyInput = InputParser.parseBooleanOrThrowError(input, "useStaticSigningKey", true); + boolean useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); + Map queryParams = params.entrySet().stream().collect(Collectors.toMap( Map.Entry::getKey, e -> e.getValue().getAsString() @@ -65,10 +75,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( main, req, resp, - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), + appIdentifier, + storage, queryParams.get("client_id"), // clientIdToCheck "/oauth2/auth", // proxyPath false, // proxyToAdmin @@ -83,8 +96,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } String redirectTo = response.headers.get("Location").get(0); + + redirectTo = OAuth.transformTokensInAuthRedirect(main, appIdentifier, storage, redirectTo, iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); List responseCookies = response.headers.get("Set-Cookie"); - + JsonObject finalResponse = new JsonObject(); finalResponse.addProperty("redirectTo", redirectTo); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java index fa40aec62..37f66ae6e 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java @@ -6,6 +6,7 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.Transformations; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.WebserverAPI; @@ -40,6 +41,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO ); if (response != null) { + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java index d508369bc..cc3c06b2c 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java @@ -6,6 +6,7 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.Transformations; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.WebserverAPI; @@ -40,6 +41,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO ); if (response != null) { + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java index ad174654c..143e505b0 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java @@ -6,6 +6,7 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.Transformations; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.WebserverAPI; @@ -40,6 +41,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO ); if (response != null) { + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java new file mode 100644 index 000000000..8e6626733 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java @@ -0,0 +1,73 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.exceptions.OAuthAPIException; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthLogoutAPI extends WebserverAPI { + public OAuthLogoutAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/sessions/logout"; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + Map queryParams = OAuthProxyHelper.defaultGetQueryParamsFromRequest(req); + OAuth.verifyIdTokenHintClientIdAndUpdateQueryParamsForLogout(main, appIdentifier, storage, queryParams); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( + main, req, resp, + appIdentifier, + storage, + queryParams.get("clientId"), // clientIdToCheck + "/oauth2/sessions/logout", // proxyPath + false, // proxyToAdmin + true, // camelToSnakeCaseConversion + queryParams, + new HashMap<>() // headers + ); + + if (response != null) { + JsonObject finalResponse = new JsonObject(); + String redirectTo = response.headers.get("Location").get(0); + + finalResponse.addProperty("status", "OK"); + finalResponse.addProperty("redirectTo", redirectTo); + + super.sendJsonResponse(200, finalResponse, resp); + } + + } catch (OAuthAPIException e) { + OAuthProxyHelper.handleOAuthAPIException(resp, e); + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | UnsupportedJWTSigningAlgorithmException | StorageTransactionLogicException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java index 3a30544d9..6764ae05d 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java @@ -92,9 +92,9 @@ public static HttpRequestForOry.Response proxyJsonPUT(Main main, HttpServletRequ public static HttpRequestForOry.Response proxyJsonDELETE(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, - JsonObject jsonInput, Map headers) throws IOException, ServletException { + Map queryParams, JsonObject jsonInput, Map headers) throws IOException, ServletException { try { - return OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); + return OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); } catch (OAuthClientNotFoundException e) { handleOAuthClientNotFoundException(resp); } catch (OAuthAPIException e) { @@ -131,7 +131,7 @@ private static void handleOAuthClientNotFoundException(HttpServletResponse resp) resp.getWriter().println(response.toString()); } - private static void handleOAuthAPIException(HttpServletResponse resp, OAuthAPIException e) throws IOException { + public static void handleOAuthAPIException(HttpServletResponse resp, OAuthAPIException e) throws IOException { JsonObject response = new JsonObject(); response.addProperty("status", "OAUTH_ERROR"); response.addProperty("error", e.error); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 22a479c6a..843fc0245 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -19,10 +19,12 @@ import com.auth0.jwt.exceptions.JWTCreationException; import com.google.gson.*; import io.supertokens.Main; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; @@ -61,19 +63,71 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String iss = InputParser.parseStringOrThrowError(input, "iss", false); // input validation JsonObject bodyFromSDK = InputParser.parseJsonObjectOrThrowError(input, "inputBody", false); - JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "access_token", true); - JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "id_token", true); + String grantType = InputParser.parseStringOrThrowError(bodyFromSDK, "grant_type", false); + JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "access_token", "authorization_code".equals(grantType)); + JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "id_token", "authorization_code".equals(grantType)); // useStaticKeyInput defaults to true, so we check if it has been explicitly set to false Boolean useStaticKeyInput = InputParser.parseBooleanOrThrowError(input, "useStaticSigningKey", true); boolean useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); + String authorizationHeader = InputParser.parseStringOrThrowError(input, "authorizationHeader", true); + + Map headers = new HashMap<>(); + if (authorizationHeader != null) { + headers.put("Authorization", authorizationHeader); + } + Map formFields = new HashMap<>(); for (Map.Entry entry : bodyFromSDK.entrySet()) { formFields.put(entry.getKey(), entry.getValue().getAsString()); } try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + // check if the refresh token is valid + if (grantType.equals("refresh_token")) { + String refreshToken = InputParser.parseStringOrThrowError(bodyFromSDK, "refresh_token", false); + + Map formFieldsForTokenIntrospect = new HashMap<>(); + formFieldsForTokenIntrospect.put("token", refreshToken); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/oauth2/introspect", // pathProxy + true, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFieldsForTokenIntrospect, + new HashMap<>() // headers + ); + + if (response == null) { + return; // proxy helper would have sent the error response + } + + JsonObject refreshTokenPayload = response.jsonResponse.getAsJsonObject(); + + try { + OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, refreshTokenPayload, refreshToken); + } catch (StorageQueryException | TenantOrAppNotFoundException | + FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + + if (!refreshTokenPayload.get("active").getAsBoolean()) { + // this is what ory would return for an invalid token + OAuthProxyHelper.handleOAuthAPIException(resp, new OAuthAPIException( + "token_inactive", "Token is inactive because it is malformed, expired or otherwise invalid. Token validation failed.", 401 + )); + return; + } + } + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( main, req, resp, getAppIdentifier(req), @@ -83,16 +137,22 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I false, // proxyToAdmin false, // camelToSnakeCaseConversion formFields, - new HashMap<>() // headers + headers // headers ); if (response != null) { try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - response.jsonResponse = OAuth.transformTokens(super.main, appIdentifier, storage, response.jsonResponse.getAsJsonObject(), iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); - } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { + + if (grantType.equals("client_credentials")) { + try { + OAuth.addM2MToken(main, appIdentifier, storage, response.jsonResponse.getAsJsonObject().get("access_token").getAsString()); + } catch (Exception e) { + // ignore + } + } + + } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { throw new ServletException(e); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java index 902e7de83..1bf281f8e 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java @@ -18,13 +18,14 @@ import com.google.gson.*; import io.supertokens.Main; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; import io.supertokens.oauth.OAuth; -import io.supertokens.oauth.Transformations; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; @@ -56,31 +57,35 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String token = InputParser.parseStringOrThrowError(input, "token", false); if (token.startsWith("st_rt_")) { - String iss = InputParser.parseStringOrThrowError(input, "iss", false); - Map formFields = new HashMap<>(); for (Map.Entry entry : input.entrySet()) { formFields.put(entry.getKey(), entry.getValue().getAsString()); } try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( main, req, resp, - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), + appIdentifier, + storage, null, // clientIdToCheck "/admin/oauth2/introspect", // pathProxy true, // proxyToAdmin false, // camelToSnakeCaseConversion - formFields, // formFields + formFields, new HashMap<>() // headers ); if (response != null) { JsonObject finalResponse = response.jsonResponse.getAsJsonObject(); - finalResponse.addProperty("iss", iss); - Transformations.transformExt(finalResponse); + try { + OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, finalResponse, token); + } catch (StorageQueryException | TenantOrAppNotFoundException | + FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } finalResponse.addProperty("status", "OK"); super.sendJsonResponse(200, finalResponse, resp); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java index 9e7f00c9b..6844eff81 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java @@ -60,6 +60,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I "/admin/clients/" + clientId, // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion + new HashMap<>(), // queryParams new JsonObject(), // jsonBody new HashMap<>() // headers ); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthSessionAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthSessionAPI.java new file mode 100644 index 000000000..ef54c7ee6 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthSessionAPI.java @@ -0,0 +1,50 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class RevokeOAuthSessionAPI extends WebserverAPI { + public RevokeOAuthSessionAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/session/revoke"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + String sessionHandle = InputParser.parseStringOrThrowError(input, "sessionHandle", false); + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + OAuth.revokeSessionHandle(main, appIdentifier, storage, sessionHandle); + + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + super.sendJsonResponse(200, response, resp); + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java new file mode 100644 index 000000000..b71ddd6d6 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java @@ -0,0 +1,141 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class RevokeOAuthTokenAPI extends WebserverAPI { + public RevokeOAuthTokenAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/token/revoke"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + String token = InputParser.parseStringOrThrowError(input, "token", false); + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + if (token.startsWith("st_rt_")) { + String gid = null; + long exp = -1; + { + // introspect token to get gid + Map formFields = new HashMap<>(); + formFields.put("token", token); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/oauth2/introspect", // pathProxy + true, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFields, + new HashMap<>() // headers + ); + + if (response != null) { + JsonObject finalResponse = response.jsonResponse.getAsJsonObject(); + + try { + OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, finalResponse, token); + if (finalResponse.get("active").getAsBoolean()) { + gid = finalResponse.get("gid").getAsString(); + exp = finalResponse.get("exp").getAsLong(); + } + } catch (StorageQueryException | TenantOrAppNotFoundException | + FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + } + } + + // revoking refresh token + String clientId = InputParser.parseStringOrThrowError(input, "client_id", false); + String clientSecret = InputParser.parseStringOrThrowError(input, "client_secret", true); + + String authorizationHeader = InputParser.parseStringOrThrowError(input, "authorizationHeader", true); + + Map headers = new HashMap<>(); + if (authorizationHeader != null) { + headers.put("Authorization", authorizationHeader); + } + + Map formFields = new HashMap<>(); + formFields.put("token", token); + formFields.put("client_id", clientId); + if (clientSecret != null) { + formFields.put("client_secret", clientSecret); + } + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + null, //clientIdToCheck + "/oauth2/revoke", // path + false, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFields, // formFields + headers // headers + ); + + if (response != null) { + // Success response would mean that the clientId/secret has been validated + if (gid != null) { + try { + OAuth.revokeRefreshToken(main, appIdentifier, storage, gid, exp); + } catch (StorageQueryException | NoSuchAlgorithmException e) { + throw new ServletException(e); + } + } + + JsonObject finalResponse = new JsonObject(); + finalResponse.addProperty("status", "OK"); + super.sendJsonResponse(200, finalResponse, resp); + } + } else { + // revoking access token + OAuth.revokeAccessToken(main, appIdentifier, storage, token); + + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + super.sendJsonResponse(200, response, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | + UnsupportedJWTSigningAlgorithmException | StorageTransactionLogicException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java new file mode 100644 index 000000000..6aef1c3ef --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java @@ -0,0 +1,70 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class RevokeOAuthTokensAPI extends WebserverAPI { + + public RevokeOAuthTokensAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/tokens/revoke"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String clientId = InputParser.parseStringOrThrowError(input, "client_id", false); + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + OAuth.revokeTokensForClientId(main, appIdentifier, storage, clientId); + + Map queryParams = new HashMap<>(); + queryParams.put("client_id", clientId); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonDELETE( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/oauth2/tokens", // proxyPath + true, // proxyToAdmin + false, // camelToSnakeCaseConversion + queryParams, // queryParams + new JsonObject(), // jsonInput + new HashMap<>() // headers + ); + + if (response != null) { + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException e) { + throw new ServletException(e); + } + } +} diff --git a/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java b/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java index bdc2e5ee1..bc27456e3 100644 --- a/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java +++ b/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java @@ -27,7 +27,6 @@ import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.oauth.OAuthAuthResponse; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; diff --git a/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java b/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java index c667ce72d..8b1169853 100644 --- a/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java +++ b/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java @@ -24,7 +24,6 @@ import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager;