diff --git a/CHANGELOG.md b/CHANGELOG.md index 07fe5c7dd..46a4d369d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,18 +53,21 @@ CREATE TABLE IF NOT EXISTS oauth_clients ( FOREIGN KEY(app_id) REFERENCES apps(app_id) ON DELETE CASCADE ); -CREATE TABLE IF NOT EXISTS oauth_revoke ( +CREATE TABLE IF NOT EXISTS oauth_sessions ( + gid VARCHAR(255), app_id VARCHAR(64) DEFAULT 'public', - target_type VARCHAR(16) NOT NULL, - target_value VARCHAR(128) NOT NULL, - timestamp BIGINT NOT NULL, + client_id VARCHAR(255) NOT NULL, + session_handle VARCHAR(128), + external_refresh_token VARCHAR(255) UNIQUE, + internal_refresh_token VARCHAR(255) UNIQUE, + jti TEXT NOT NULL, exp BIGINT NOT NULL, - PRIMARY KEY (app_id, target_type, target_value), - FOREIGN KEY(app_id) REFERENCES apps(app_id) ON DELETE CASCADE + PRIMARY KEY (gid), + FOREIGN KEY(app_id, client_id) REFERENCES oauth_clients(app_id, client_id) ON DELETE CASCADE ); -CREATE INDEX IF NOT EXISTS oauth_revoke_timestamp_index ON oauth_revoke(timestamp DESC, app_id DESC); -CREATE INDEX IF NOT EXISTS oauth_revoke_exp_index ON oauth_revoke(exp DESC); +CREATE INDEX IF NOT EXISTS oauth_session_exp_index ON oauth_sessions(exp DESC); +CREATE INDEX IF NOT EXISTS oauth_session_external_refresh_token_index ON oauth_sessions(app_id, external_refresh_token DESC); CREATE TABLE IF NOT EXISTS oauth_m2m_tokens ( app_id VARCHAR(64) DEFAULT 'public', @@ -104,18 +107,22 @@ CREATE TABLE IF NOT EXISTS oauth_clients ( FOREIGN KEY(app_id) REFERENCES apps(app_id) ON DELETE CASCADE ); -CREATE TABLE IF NOT EXISTS oauth_revoke ( + +CREATE TABLE IF NOT EXISTS oauth_sessions ( + gid VARCHAR(255), app_id VARCHAR(64) DEFAULT 'public', - target_type VARCHAR(16) NOT NULL, - target_value VARCHAR(128) NOT NULL, - timestamp BIGINT UNSIGNED NOT NULL, - exp BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (app_id, target_type, target_value), - FOREIGN KEY(app_id) REFERENCES apps(app_id) ON DELETE CASCADE + client_id VARCHAR(255) NOT NULL, + session_handle VARCHAR(128), + external_refresh_token VARCHAR(255) UNIQUE, + internal_refresh_token VARCHAR(255) UNIQUE, + jti TEXT NOT NULL, + exp BIGINT NOT NULL, + PRIMARY KEY (gid), + FOREIGN KEY(app_id, client_id) REFERENCES oauth_clients(app_id, client_id) ON DELETE CASCADE ); -CREATE INDEX oauth_revoke_timestamp_index ON oauth_revoke(timestamp DESC, app_id DESC); -CREATE INDEX oauth_revoke_exp_index ON oauth_revoke(exp DESC); +CREATE INDEX IF NOT EXISTS oauth_session_exp_index ON oauth_sessions(exp DESC); +CREATE INDEX IF NOT EXISTS oauth_session_external_refresh_token_index ON oauth_sessions(app_id, external_refresh_token DESC); CREATE TABLE oauth_m2m_tokens ( app_id VARCHAR(64) DEFAULT 'public', diff --git a/src/main/java/io/supertokens/Main.java b/src/main/java/io/supertokens/Main.java index 7a4fded61..29d05c206 100644 --- a/src/main/java/io/supertokens/Main.java +++ b/src/main/java/io/supertokens/Main.java @@ -20,7 +20,7 @@ import io.supertokens.config.Config; import io.supertokens.config.CoreConfig; import io.supertokens.cronjobs.Cronjobs; -import io.supertokens.cronjobs.cleanupOAuthRevokeListAndChallenges.CleanupOAuthRevokeListAndChallenges; +import io.supertokens.cronjobs.cleanupOAuthRevokeListAndChallenges.CleanupOAuthSessionsAndChallenges; import io.supertokens.cronjobs.deleteExpiredAccessTokenSigningKeys.DeleteExpiredAccessTokenSigningKeys; import io.supertokens.cronjobs.deleteExpiredDashboardSessions.DeleteExpiredDashboardSessions; import io.supertokens.cronjobs.deleteExpiredEmailVerificationTokens.DeleteExpiredEmailVerificationTokens; @@ -257,7 +257,7 @@ 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, CleanupOAuthRevokeListAndChallenges.init(this, uniqueUserPoolIdsTenants)); + Cronjobs.addCronjob(this, CleanupOAuthSessionsAndChallenges.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/cleanupOAuthRevokeListAndChallenges/CleanupOAuthRevokeListAndChallenges.java b/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeListAndChallenges/CleanupOAuthSessionsAndChallenges.java similarity index 78% rename from src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeListAndChallenges/CleanupOAuthRevokeListAndChallenges.java rename to src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeListAndChallenges/CleanupOAuthSessionsAndChallenges.java index c3cfa3cbd..495007b47 100644 --- a/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeListAndChallenges/CleanupOAuthRevokeListAndChallenges.java +++ b/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeListAndChallenges/CleanupOAuthSessionsAndChallenges.java @@ -1,7 +1,5 @@ package io.supertokens.cronjobs.cleanupOAuthRevokeListAndChallenges; -import java.util.List; - import io.supertokens.Main; import io.supertokens.cronjobs.CronTask; import io.supertokens.cronjobs.CronTaskTest; @@ -11,19 +9,21 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.oauth.OAuthStorage; -public class CleanupOAuthRevokeListAndChallenges extends CronTask { +import java.util.List; + +public class CleanupOAuthSessionsAndChallenges extends CronTask { public static final String RESOURCE_KEY = "io.supertokens.cronjobs.cleanupOAuthRevokeListAndChallenges" + ".CleanupOAuthRevokeListAndChallenges"; - private CleanupOAuthRevokeListAndChallenges(Main main, List> tenantsInfo) { + private CleanupOAuthSessionsAndChallenges(Main main, List> tenantsInfo) { super("CleanupOAuthRevokeList", main, tenantsInfo, true); } - public static CleanupOAuthRevokeListAndChallenges init(Main main, List> tenantsInfo) { - return (CleanupOAuthRevokeListAndChallenges) main.getResourceDistributor() + public static CleanupOAuthSessionsAndChallenges init(Main main, List> tenantsInfo) { + return (CleanupOAuthSessionsAndChallenges) main.getResourceDistributor() .setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY, - new CleanupOAuthRevokeListAndChallenges(main, tenantsInfo)); + new CleanupOAuthSessionsAndChallenges(main, tenantsInfo)); } @Override @@ -34,7 +34,7 @@ protected void doTaskPerStorage(Storage storage) throws Exception { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); long monthAgo = System.currentTimeMillis() / 1000 - 31 * 24 * 3600; - oauthStorage.deleteExpiredRevokedOAuthTokens(monthAgo); + oauthStorage.deleteExpiredOAuthSessions(monthAgo); oauthStorage.deleteExpiredOAuthM2MTokens(monthAgo); oauthStorage.deleteOAuthLogoutChallengesBefore(System.currentTimeMillis() - 1000 * 60 * 60 * 48); // 48 hours diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 2d7513a8b..06684ce17 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -49,7 +49,10 @@ import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo; import io.supertokens.pluginInterface.jwt.exceptions.DuplicateKeyIdException; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; -import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.MultitenancyStorage; +import io.supertokens.pluginInterface.multitenancy.TenantConfig; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateClientTypeException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; @@ -57,7 +60,6 @@ import io.supertokens.pluginInterface.multitenancy.sqlStorage.MultitenancySQLStorage; import io.supertokens.pluginInterface.oauth.OAuthClient; import io.supertokens.pluginInterface.oauth.OAuthLogoutChallenge; -import io.supertokens.pluginInterface.oauth.OAuthRevokeTargetType; import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.pluginInterface.oauth.exception.DuplicateOAuthLogoutChallengeException; import io.supertokens.pluginInterface.oauth.exception.OAuthClientNotFoundException; @@ -3070,42 +3072,39 @@ public List getOAuthClients(AppIdentifier appIdentifier, List jtis, long exp) + throws StorageQueryException, OAuthClientNotFoundException { try { - OAuthQueries.createOrUpdateRefreshTokenMapping(this, appIdentifier, superTokensRefreshToken, oauthProviderRefreshToken, exp); + OAuthQueries.createOrUpdateOAuthSession(this, appIdentifier, gid, clientId, externalRefreshToken, + internalRefreshToken, sessionHandle, jtis, exp); } catch (SQLException e) { - throw new StorageQueryException(e); - } - } + if (e instanceof SQLiteException) { + String errorMessage = e.getMessage(); + SQLiteConfig config = Config.getConfig(this); - @Override - public String getRefreshTokenMapping(AppIdentifier appIdentifier, String superTokensRefreshToken) - throws StorageQueryException { - try { - return OAuthQueries.getRefreshTokenMapping(this, appIdentifier, superTokensRefreshToken); - } catch (SQLException e) { + if (isForeignKeyConstraintError( + errorMessage, + config.getOAuthClientsTable(), + new String[]{"app_id", "client_id"}, + new Object[]{appIdentifier.getAppId(), clientId})) { + throw new OAuthClientNotFoundException(); + } + } throw new StorageQueryException(e); } } @Override - public void deleteRefreshTokenMapping(AppIdentifier appIdentifier, String superTokensRefreshToken) + public String getRefreshTokenMapping(AppIdentifier appIdentifier, String externalRefreshToken) throws StorageQueryException { try { - OAuthQueries.deleteRefreshTokenMapping(this, appIdentifier, superTokensRefreshToken); + return OAuthQueries.getRefreshTokenMapping(this, appIdentifier, externalRefreshToken); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public void deleteExpiredRefreshTokenMappings(long exp) throws StorageQueryException { + public void deleteExpiredOAuthSessions(long exp) throws StorageQueryException { try { - OAuthQueries.deleteExpiredRefreshTokenMappings(this, exp); + OAuthQueries.deleteExpiredOAuthSessions(this, exp); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -3269,4 +3273,23 @@ public int countTotalNumberOfOAuthM2MTokensAlive(AppIdentifier appIdentifier) th throw new StorageQueryException(e); } } + + @Override + public boolean isOAuthTokenRevokedByGID(AppIdentifier appIdentifier, String gid) throws StorageQueryException { + try { + return !OAuthQueries.isOAuthSessionExistsByGID(this, appIdentifier, gid); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public boolean isOAuthTokenRevokedByJTI(AppIdentifier appIdentifier, String gid, String jti) + throws StorageQueryException { + try { + return !OAuthQueries.isOAuthSessionExistsByJTI(this, appIdentifier, gid, jti); + } 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 86bcd9fa6..0790898dc 100644 --- a/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java +++ b/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java @@ -173,14 +173,14 @@ public String getOAuthRefreshTokenMappingTable() { return "oauth_refresh_token_mapping"; } - public String getOAuthRevokeTable() { - return "oauth_revoke"; - } - public String getOAuthM2MTokensTable() { return "oauth_m2m_tokens"; } + public String getOAuthSessionsTable() { + return "oauth_sessions"; + } + public String getOAuthLogoutChallengesTable() { return "oauth_logout_challenges"; } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java index f17b17bd4..eb2fe4809 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java @@ -432,21 +432,13 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc update(start, OAuthQueries.getQueryToCreateOAuthClientTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getOAuthRefreshTokenMappingTable())) { + if (!doesTableExists(start, Config.getConfig(start).getOAuthSessionsTable())) { getInstance(main).addState(CREATING_NEW_TABLE, null); - update(start, OAuthQueries.getQueryToCreateOAuthRefreshTokenMappingTable(start), NO_OP_SETTER); + update(start, OAuthQueries.getQueryToCreateOAuthSessionsTable(start), NO_OP_SETTER); // index - update(start, OAuthQueries.getQueryToCreateOAuthRefreshTokenMappingExpIndex(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); - update(start, OAuthQueries.getQueryToCreateOAuthRevokeExpIndex(start), NO_OP_SETTER); + update(start, OAuthQueries.getQueryToCreateOAuthSessionsExpIndex(start), NO_OP_SETTER); + update(start, OAuthQueries.getQueryToCreateOAuthSessionsExternalRefreshTokenIndex(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getOAuthM2MTokensTable())) { diff --git a/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java index ab08a9ffc..f68e2efed 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java @@ -23,12 +23,13 @@ import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.oauth.OAuthClient; import io.supertokens.pluginInterface.oauth.OAuthLogoutChallenge; -import io.supertokens.pluginInterface.oauth.OAuthRevokeTargetType; +import org.jetbrains.annotations.NotNull; -import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute; import static io.supertokens.inmemorydb.QueryExecutorTemplate.update; @@ -49,51 +50,33 @@ public static String getQueryToCreateOAuthClientTable(Start start) { // @formatter:on } - public static String getQueryToCreateOAuthRefreshTokenMappingTable(Start start) { - String oAuth2RefreshTokenMappingTable = Config.getConfig(start).getOAuthRefreshTokenMappingTable(); + public static String getQueryToCreateOAuthSessionsTable(Start start) { + String oAuthSessionsTable = Config.getConfig(start).getOAuthSessionsTable(); // @formatter:off - return "CREATE TABLE IF NOT EXISTS " + oAuth2RefreshTokenMappingTable + " (" + return "CREATE TABLE IF NOT EXISTS " + oAuthSessionsTable + " (" + + "gid VARCHAR(255)," // needed for instrospect. It's much easier to find these records if we have a gid + "app_id VARCHAR(64) DEFAULT 'public'," - + "st_refresh_token VARCHAR(255) NOT NULL," - + "op_refresh_token VARCHAR(255) NOT NULL," - + "exp BIGINT NOT NULL," - + "PRIMARY KEY (app_id, st_refresh_token)," - + "FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE);"; - // @formatter:on - } - - public static String getQueryToCreateOAuthRefreshTokenMappingExpIndex(Start start) { - String oAuth2RefreshTokenMappingTable = Config.getConfig(start).getOAuthRefreshTokenMappingTable(); - return "CREATE INDEX IF NOT EXISTS oauth_refresh_token_mapping_exp_index ON " - + oAuth2RefreshTokenMappingTable + "(exp DESC);"; - } - - 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, " + + "client_id VARCHAR(255) NOT NULL," + + "session_handle VARCHAR(128)," + + "external_refresh_token VARCHAR(255) UNIQUE," + + "internal_refresh_token VARCHAR(255) UNIQUE," + + "jti TEXT NOT NULL," // comma separated jti list + "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" - + ");"; + + "PRIMARY KEY (gid)," + + "FOREIGN KEY(app_id, client_id) REFERENCES " + Config.getConfig(start).getOAuthClientsTable() + "(app_id, client_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 getQueryToCreateOAuthSessionsExpIndex(Start start) { + String oAuth2SessionTable = Config.getConfig(start).getOAuthSessionsTable(); + return "CREATE INDEX IF NOT EXISTS oauth_session_exp_index ON " + + oAuth2SessionTable + "(exp DESC);"; } - public static String getQueryToCreateOAuthRevokeExpIndex(Start start) { - String oAuth2RevokeTable = Config.getConfig(start).getOAuthRevokeTable(); - return "CREATE INDEX IF NOT EXISTS oauth_revoke_exp_index ON " - + oAuth2RevokeTable + "(exp DESC);"; + public static String getQueryToCreateOAuthSessionsExternalRefreshTokenIndex(Start start) { + String oAuth2SessionTable = Config.getConfig(start).getOAuthSessionsTable(); + return "CREATE INDEX IF NOT EXISTS oauth_session_external_refresh_token_index ON " + + oAuth2SessionTable + "(app_id, external_refresh_token DESC);"; } public static String getQueryToCreateOAuthM2MTokensTable(Start start) { @@ -163,6 +146,34 @@ public static OAuthClient getOAuthClientById(Start start, String clientId, AppId }); } + public static void createOrUpdateOAuthSession(Start start, AppIdentifier appIdentifier, @NotNull String gid, @NotNull String clientId, + String externalRefreshToken, String internalRefreshToken, String sessionHandle, + List jtis, long exp) + throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getOAuthSessionsTable() + + " (gid, client_id, app_id, external_refresh_token, internal_refresh_token, session_handle, jti, exp) VALUES (?, ?, ?, ?, ?, ?, ?, ?) " + + "ON CONFLICT (gid) DO UPDATE SET external_refresh_token = ?, internal_refresh_token = ?, " + + "session_handle = ? , jti = CONCAT(jti, ',' , ?), exp = ?"; + update(start, QUERY, pst -> { + String jtiDbValue = jtis == null ? null : String.join(",", jtis); + + pst.setString(1, gid); + pst.setString(2, clientId); + pst.setString(3, appIdentifier.getAppId()); + pst.setString(4, externalRefreshToken); + pst.setString(5, internalRefreshToken); + pst.setString(6, sessionHandle); + pst.setString(7, jtiDbValue); + pst.setLong(8, exp); + + pst.setString(9, externalRefreshToken); + pst.setString(10, internalRefreshToken); + pst.setString(11, sessionHandle); + pst.setString(12, jtiDbValue); + pst.setLong(13, exp); + }); + } + public static List getOAuthClients(Start start, AppIdentifier appIdentifier, List clientIds) throws SQLException, StorageQueryException { String QUERY = "SELECT * FROM " + Config.getConfig(start).getOAuthClientsTable() @@ -212,51 +223,51 @@ public static boolean deleteOAuthClient(Start start, String clientId, AppIdentif return numberOfRow > 0; } - public static void revokeOAuthTokensBasedOnTargetFields(Start start, AppIdentifier appIdentifier, OAuthRevokeTargetType targetType, String targetValue, long exp) + public static boolean deleteOAuthSessionByGID(Start start, AppIdentifier appIdentifier, String gid) 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 = ?"; + String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE gid = ? and app_id = ?;"; + int numberOfRows = update(start, DELETE, pst -> { + pst.setString(1, gid); + pst.setString(2, appIdentifier.getAppId()); + }); + return numberOfRows > 0; + } - long currentTime = System.currentTimeMillis() / 1000; - update(start, INSERT, pst -> { + public static boolean deleteOAuthSessionByClientId(Start start, AppIdentifier appIdentifier, String clientId) + throws SQLException, StorageQueryException { + String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE app_id = ? and client_id = ?;"; + int numberOfRows = update(start, DELETE, pst -> { pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, targetType.getValue()); - pst.setString(3, targetValue); - pst.setLong(4, currentTime); - pst.setLong(5, exp); - pst.setLong(6, currentTime); - pst.setLong(7, exp); + pst.setString(2, clientId); }); + return numberOfRows > 0; } - public static boolean isOAuthTokenRevokedBasedOnTargetFields(Start start, AppIdentifier appIdentifier, OAuthRevokeTargetType[] targetTypes, String[] targetValues, long issuedAt) + public static boolean deleteOAuthSessionBySessionHandle(Start start, AppIdentifier appIdentifier, String sessionHandle) 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 -> { + String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE app_id = ? and session_handle = ?"; + int numberOfRows = update(start, DELETE, 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].getValue()); - index++; - pst.setString(index, targetValues[i]); - index++; - } - }, ResultSet::next); + pst.setString(2, sessionHandle); + }); + return numberOfRows > 0; + } + + public static boolean deleteJTIFromOAuthSession(Start start, AppIdentifier appIdentifier, String gid, String jti) + throws SQLException, StorageQueryException { + //jti is a comma separated list. When deleting a jti, just have to delete from the list + String DELETE = "UPDATE " + Config.getConfig(start).getOAuthSessionsTable() + + " SET jti = REPLACE(jti, ?, '')" // deletion means replacing the jti with empty char + + " WHERE app_id = ? and gid = ?"; + int numberOfRows = update(start, DELETE, pst -> { + pst.setString(1, jti); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, gid); + }); + return numberOfRows > 0; } public static int countTotalNumberOfClients(Start start, AppIdentifier appIdentifier, @@ -329,16 +340,6 @@ public static void addOAuthM2MTokenForStats(Start start, AppIdentifier appIdenti }); } - public static void deleteExpiredRevokedOAuthTokens(Start start, long exp) throws SQLException, StorageQueryException { - // delete expired revoked tokens - String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthRevokeTable() + - " WHERE exp < ?"; - - update(start, QUERY, pst -> { - pst.setLong(1, exp); - }); - } - public static void addOAuthLogoutChallenge(Start start, AppIdentifier appIdentifier, String challenge, String clientId, String postLogoutRedirectionUri, String sessionHandle, String state, long timeCreated) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getOAuthLogoutChallengesTable() + @@ -394,40 +395,27 @@ public static void deleteOAuthLogoutChallengesBefore(Start start, long time) thr }); } - public static void createOrUpdateRefreshTokenMapping(Start start, AppIdentifier appIdentifier, String superTokensRefreshToken, String oauthProviderRefreshToken, long exp) throws SQLException, StorageQueryException { - String QUERY = "INSERT INTO " + Config.getConfig(start).getOAuthRefreshTokenMappingTable() + - " (app_id, st_refresh_token, op_refresh_token, exp) VALUES (?, ?, ?, ?) ON CONFLICT (app_id, st_refresh_token) DO UPDATE SET op_refresh_token = ?, exp = ?"; - update(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, superTokensRefreshToken); - pst.setString(3, oauthProviderRefreshToken); - pst.setLong(4, exp); - pst.setString(5, oauthProviderRefreshToken); - pst.setLong(6, exp); - }); - } - - public static String getRefreshTokenMapping(Start start, AppIdentifier appIdentifier, String superTokensRefreshToken) throws SQLException, StorageQueryException { - String QUERY = "SELECT op_refresh_token FROM " + Config.getConfig(start).getOAuthRefreshTokenMappingTable() + - " WHERE app_id = ? AND st_refresh_token = ?"; + public static String getRefreshTokenMapping(Start start, AppIdentifier appIdentifier, String externalRefreshToken) throws SQLException, StorageQueryException { + String QUERY = "SELECT internal_refresh_token FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE app_id = ? AND external_refresh_token = ?"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, superTokensRefreshToken); + pst.setString(2, externalRefreshToken); }, result -> { if (result.next()) { - return result.getString("op_refresh_token"); + return result.getString("internal_refresh_token"); } return null; }); } - public static void deleteRefreshTokenMapping(Start start, AppIdentifier appIdentifier, - String superTokensRefreshToken) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthRefreshTokenMappingTable() + - " WHERE app_id = ? AND st_refresh_token = ?"; + public static void deleteExpiredOAuthSessions(Start start, long exp) throws SQLException, StorageQueryException { + // delete expired M2M tokens + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE exp < ?"; + update(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, superTokensRefreshToken); + pst.setLong(1, exp); }); } @@ -435,17 +423,41 @@ public static void deleteExpiredOAuthM2MTokens(Start start, long exp) throws SQL // delete expired M2M tokens String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + " WHERE exp < ?"; - update(start, QUERY, pst -> { pst.setLong(1, exp); }); } - public static void deleteExpiredRefreshTokenMappings(Start start, long exp) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthRefreshTokenMappingTable() + - " WHERE exp < ?"; - update(start, QUERY, pst -> { - pst.setLong(1, exp); + public static boolean isOAuthSessionExistsByJTI(Start start, AppIdentifier appIdentifier, String gid, String jti) + throws SQLException, StorageQueryException { + String SELECT = "SELECT jti FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE app_id = ? and gid = ?;"; + return execute(start, SELECT, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, gid); + }, result -> { + if(result.next()){ + List jtis = Arrays.stream(result.getString(1).split(",")).filter(s -> !s.isEmpty()).collect( + Collectors.toList()); + return jtis.contains(jti); + } + return false; }); } + + public static boolean isOAuthSessionExistsByGID(Start start, AppIdentifier appIdentifier, String gid) + throws SQLException, StorageQueryException { + String SELECT = "SELECT count(*) FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE app_id = ? and gid = ?;"; + return execute(start, SELECT, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, gid); + }, result -> { + if(result.next()){ + return result.getInt(1) > 0; + } + return false; + }); + } + } diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 3e1fc0544..7501afaca 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -20,7 +20,6 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; - import io.supertokens.Main; import io.supertokens.config.Config; import io.supertokens.exceptions.TryRefreshTokenException; @@ -28,23 +27,26 @@ import io.supertokens.featureflag.FeatureFlag; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; -import io.supertokens.oauth.exceptions.*; +import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.StorageUtils; 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.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.oauth.OAuthClient; import io.supertokens.pluginInterface.oauth.OAuthLogoutChallenge; -import io.supertokens.pluginInterface.oauth.OAuthRevokeTargetType; import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.pluginInterface.oauth.exception.DuplicateOAuthLogoutChallengeException; import io.supertokens.pluginInterface.oauth.exception.OAuthClientNotFoundException; import io.supertokens.session.jwt.JWT.JWTException; import io.supertokens.utils.Utils; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; @@ -55,10 +57,6 @@ import java.util.*; import java.util.Map.Entry; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; - public class OAuth { private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException { @@ -362,24 +360,31 @@ public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, public static void addOrUpdateClient(Main main, AppIdentifier appIdentifier, Storage storage, String clientId, String clientSecret, boolean isClientCredentialsOnly, boolean enableRefreshTokenRotation) throws StorageQueryException, TenantOrAppNotFoundException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, InvalidConfigException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - clientSecret = encryptClientSecret(main, clientSecret); + clientSecret = encryptClientSecret(main, appIdentifier.getAsPublicTenantIdentifier(), clientSecret); oauthStorage.addOrUpdateOauthClient(appIdentifier, clientId, clientSecret, isClientCredentialsOnly, enableRefreshTokenRotation); } - private static String encryptClientSecret(Main main, String clientSecret) throws InvalidConfigException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + + private static String encryptClientSecret(Main main, TenantIdentifier tenant, String clientSecret) + throws InvalidConfigException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, + NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, + TenantOrAppNotFoundException { if (clientSecret == null) { return null; } - String key = Config.getConfig(main).getOAuthClientSecretEncryptionKey(); + String key = Config.getConfig(tenant, main).getOAuthClientSecretEncryptionKey(); clientSecret = Utils.encrypt(clientSecret, key); return clientSecret; } - private static String decryptClientSecret(Main main, String clientSecret) throws InvalidConfigException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + private static String decryptClientSecret(Main main, TenantIdentifier tenant, String clientSecret) + throws InvalidConfigException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, + NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, + TenantOrAppNotFoundException { if (clientSecret == null) { return null; } - String key = Config.getConfig(main).getOAuthClientSecretEncryptionKey(); + String key = Config.getConfig(tenant, main).getOAuthClientSecretEncryptionKey(); clientSecret = Utils.decrypt(clientSecret, key); return clientSecret; } @@ -389,12 +394,15 @@ public static boolean removeClient(Main main, AppIdentifier appIdentifier, Stora return oauthStorage.deleteOAuthClient(appIdentifier, clientId); } - public static List getClients(Main main, AppIdentifier appIdentifier, Storage storage, List clientIds) throws StorageQueryException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, InvalidConfigException { + public static List getClients(Main main, AppIdentifier appIdentifier, Storage storage, List clientIds) + throws StorageQueryException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, + NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, + InvalidConfigException, TenantOrAppNotFoundException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); List finalResult = new ArrayList<>(); List clients = oauthStorage.getOAuthClients(appIdentifier, clientIds); for (OAuthClient client : clients) { - finalResult.add(new OAuthClient(client.clientId, decryptClientSecret(main, client.clientSecret), client.isClientCredentialsOnly, client.enableRefreshTokenRotation)); + finalResult.add(new OAuthClient(client.clientId, decryptClientSecret(main, appIdentifier.getAsPublicTenantIdentifier(), client.clientSecret), client.isClientCredentialsOnly, client.enableRefreshTokenRotation)); } return finalResult; } @@ -444,7 +452,7 @@ 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 { + Storage storage, JsonObject payload, String refreshToken, String clientId) throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -461,50 +469,42 @@ public static void verifyAndUpdateIntrospectRefreshTokenPayload(Main main, AppId 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 - // } + refreshToken = refreshToken.replace("st_rt_", "ory_rt_"); + Map formFields = new HashMap<>(); + formFields.put("token", refreshToken); + + try { + OAuthClient oAuthClient = OAuth.getOAuthClientById(main, appIdentifier, storage, clientId); + formFields.put("client_secret", oAuthClient.clientSecret); + formFields.put("client_id", oAuthClient.clientId); + + HttpRequestForOAuthProvider.Response revokeResponse = doOAuthProxyFormPOST( + main, appIdentifier, oauthStorage, + clientId, // clientIdToCheck + "/oauth2/revoke", // path + false, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFields, + new HashMap<>()); + + } catch (OAuthAPIException | OAuthClientNotFoundException | InvalidKeyException | NoSuchAlgorithmException | + InvalidKeySpecException | NoSuchPaddingException | InvalidAlgorithmParameterException | + IllegalBlockSizeException | BadPaddingException 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(OAuthRevokeTargetType.CLIENT_ID); - targetValues.add(payload.get("client_id").getAsString()); - - if (payload.has("jti")) { - targetTypes.add(OAuthRevokeTargetType.JTI); - targetValues.add(payload.get("jti").getAsString()); - } - - if (payload.has("gid")) { - targetTypes.add(OAuthRevokeTargetType.GID); - targetValues.add(payload.get("gid").getAsString()); - } - - if (payload.has("sessionHandle")) { - targetTypes.add(OAuthRevokeTargetType.SESSION_HANDLE); - targetValues.add(payload.get("sessionHandle").getAsString()); + boolean revoked = true; + if (payload.has("jti") && payload.has("gid")) { + //access token + revoked = oauthStorage.isOAuthTokenRevokedByJTI(appIdentifier, payload.get("gid").getAsString(), payload.get("jti").getAsString()); + } else { + // refresh token + revoked = oauthStorage.isOAuthTokenRevokedByGID(appIdentifier, payload.get("gid").getAsString()); } - - return oauthStorage.isOAuthTokenRevokedBasedOnTargetFields(appIdentifier, targetTypes.toArray(new OAuthRevokeTargetType[0]), targetValues.toArray(new String[0]), issuedAt); + return revoked; } public static JsonObject introspectAccessToken(Main main, AppIdentifier appIdentifier, Storage storage, @@ -538,28 +538,25 @@ public static JsonObject introspectAccessToken(Main main, AppIdentifier appIdent public static void revokeTokensForClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException, TenantOrAppNotFoundException { - long exp = System.currentTimeMillis() / 1000 + 3600 * 24 * 183; // 6 month from now OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - oauthStorage.revokeOAuthTokensBasedOnTargetFields(appIdentifier, OAuthRevokeTargetType.CLIENT_ID, clientId, exp); + oauthStorage.revokeOAuthTokenByClientId(appIdentifier, clientId); } - public static void revokeRefreshToken(Main main, AppIdentifier appIdentifier, Storage storage, String gid, long exp) + public static void revokeRefreshToken(Main main, AppIdentifier appIdentifier, Storage storage, String gid) throws StorageQueryException, NoSuchAlgorithmException, TenantOrAppNotFoundException { - OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - oauthStorage.revokeOAuthTokensBasedOnTargetFields(appIdentifier, OAuthRevokeTargetType.GID, gid, exp); - } + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.revokeOAuthTokenByGID(appIdentifier, gid); + } 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.revokeOAuthTokensBasedOnTargetFields(appIdentifier, OAuthRevokeTargetType.JTI, jti, exp); + String gid = payload.get("gid").getAsString(); + oauthStorage.revokeOAuthTokenByJTI(appIdentifier, gid, jti); } } catch (TryRefreshTokenException e) { @@ -567,12 +564,11 @@ public static void revokeAccessToken(Main main, AppIdentifier appIdentifier, } } - public static void revokeSessionHandle(Main main, AppIdentifier appIdentifier, Storage storage, - String sessionHandle) throws StorageQueryException, TenantOrAppNotFoundException { - long exp = System.currentTimeMillis() / 1000 + 3600 * 24 * 183; // 6 month from now - OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - oauthStorage.revokeOAuthTokensBasedOnTargetFields(appIdentifier, OAuthRevokeTargetType.SESSION_HANDLE, sessionHandle, exp); - } + public static void revokeSessionHandle(Main main, AppIdentifier appIdentifier, Storage storage, + String sessionHandle) throws StorageQueryException, TenantOrAppNotFoundException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.revokeOAuthTokenBySessionHandle(appIdentifier, sessionHandle); + } public static JsonObject verifyIdTokenAndGetPayload(Main main, AppIdentifier appIdentifier, Storage storage, String idToken) throws StorageQueryException, OAuthAPIException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { @@ -638,35 +634,35 @@ public static void deleteLogoutChallenge(Main main, AppIdentifier appIdentifier, oauthStorage.deleteOAuthLogoutChallenge(appIdentifier, challenge); } - public static OAuthClient getOAuthClientById(Main main, AppIdentifier appIdentifier, Storage storage, - String clientId) throws OAuthClientNotFoundException, StorageQueryException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, InvalidConfigException { + public static OAuthClient getOAuthClientById(Main main, AppIdentifier appIdentifier, Storage storage, + String clientId) + throws OAuthClientNotFoundException, StorageQueryException, InvalidKeyException, NoSuchAlgorithmException, + InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, + IllegalBlockSizeException, BadPaddingException, InvalidConfigException, TenantOrAppNotFoundException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); OAuthClient client = oauthStorage.getOAuthClientById(appIdentifier, clientId); if (client.clientSecret != null) { - client = new OAuthClient(client.clientId, decryptClientSecret(main, client.clientSecret), client.isClientCredentialsOnly, client.enableRefreshTokenRotation); + client = new OAuthClient(client.clientId, decryptClientSecret(main, appIdentifier.getAsPublicTenantIdentifier(), client.clientSecret), client.isClientCredentialsOnly, client.enableRefreshTokenRotation); } return client; - } - - public static String getOAuthProviderRefreshToken(Main main, AppIdentifier appIdentifier, Storage storage, - String refreshToken) throws StorageQueryException { - OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - String opRefreshToken = oauthStorage.getRefreshTokenMapping(appIdentifier, refreshToken); - if (opRefreshToken == null) { - return refreshToken; - } - return opRefreshToken; } - public static void createOrUpdateRefreshTokenMapping(Main main, AppIdentifier appIdentifier, Storage storage, - String inputRefreshToken, String newRefreshToken, long exp) throws StorageQueryException { + public static String getInternalRefreshToken(Main main, AppIdentifier appIdentifier, Storage storage, + String externalRefreshToken) throws StorageQueryException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - oauthStorage.createOrUpdateRefreshTokenMapping(appIdentifier, inputRefreshToken, newRefreshToken, exp); + String internalRefreshToken = oauthStorage.getRefreshTokenMapping(appIdentifier, externalRefreshToken); + if (internalRefreshToken == null) { + return externalRefreshToken; + } + return internalRefreshToken; } - public static void deleteRefreshTokenMappingIfExists(Main main, AppIdentifier appIdentifier, Storage storage, - String inputRefreshToken) throws StorageQueryException { + public static void createOrUpdateOauthSession(Main main, AppIdentifier appIdentifier, Storage storage, + String clientId, String gid, String externalRefreshToken, String internalRefreshToken, + String sessionHandle, List jtis, long exp) + throws StorageQueryException, OAuthClientNotFoundException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - oauthStorage.deleteRefreshTokenMapping(appIdentifier, inputRefreshToken); + oauthStorage.createOrUpdateOAuthSession(appIdentifier, gid, clientId, externalRefreshToken, internalRefreshToken, + sessionHandle, jtis, exp); } } diff --git a/src/main/java/io/supertokens/oauth/OAuthToken.java b/src/main/java/io/supertokens/oauth/OAuthToken.java index 0a9ac61df..6500e4194 100644 --- a/src/main/java/io/supertokens/oauth/OAuthToken.java +++ b/src/main/java/io/supertokens/oauth/OAuthToken.java @@ -25,10 +25,7 @@ import java.security.KeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; public class OAuthToken { public enum TokenType { @@ -108,6 +105,11 @@ public static String reSignToken(AppIdentifier appIdentifier, Main main, String if (tokenType == TokenType.ACCESS_TOKEN) { // we need to move rsub, tId and sessionHandle from ext to root Transformations.transformExt(payload); + } else { + if (payload.has("ext")) { + JsonObject ext = payload.get("ext").getAsJsonObject(); + payload.addProperty("sid", ext.get("sessionHandle").getAsString()); + } } // This should only happen in the authorization code flow during the token exchange. (enforced on the api level) @@ -125,6 +127,12 @@ public static String reSignToken(AppIdentifier appIdentifier, Main main, String payload.remove("ext"); payload.remove("initialPayload"); + // We ensure that the gid is there + // If it isn't that means that we are in a client_credentials (M2M) flow + if (!payload.has("gid")) { + payload.addProperty("gid", UUID.randomUUID().toString()); + } + 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/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java index 6b8d83661..2b1fce705 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java @@ -47,6 +47,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO accessToken.add("initialPayload", initialAccessTokenPayload); JsonObject idToken = new JsonObject(); + JsonObject idTokenExt = new JsonObject(); + idTokenExt.addProperty("sessionHandle", sessionHandle); + idToken.add("ext", idTokenExt); idToken.add("initialPayload", initialIdTokenPayload); // remove the above from input 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 5ee7e2652..3850ada87 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -19,21 +19,22 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; - import io.supertokens.ActiveUsers; import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOAuthProvider; import io.supertokens.oauth.OAuth; -import io.supertokens.oauth.OAuthToken; 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.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.oauth.exception.OAuthClientNotFoundException; import io.supertokens.pluginInterface.session.SessionInfo; import io.supertokens.pluginInterface.useridmapping.UserIdMapping; import io.supertokens.session.Session; +import io.supertokens.session.jwt.JWT; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.useridmapping.UserIdType; import io.supertokens.webserver.InputParser; @@ -107,21 +108,34 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I redirectTo = OAuth.transformTokensInAuthRedirect(main, appIdentifier, storage, redirectTo, iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); - if (redirectTo.contains("#")) { String tokensPart = redirectTo.substring(redirectTo.indexOf("#") + 1); String[] parts = tokensPart.split("&"); for (String part : parts) { if (part.startsWith("access_token=")) { String accessToken = java.net.URLDecoder.decode(part.split("=")[1], "UTF-8"); + JsonObject accessTokenPayload; try { - JsonObject accessTokenPayload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, accessToken); - if (accessTokenPayload.has("sessionHandle")) { - updateLastActive(appIdentifier, accessTokenPayload.get("sessionHandle").getAsString()); - } - } catch (Exception e) { - // ignore + JWT.JWTInfo jwtInfo = JWT.getPayloadWithoutVerifying(accessToken); + accessTokenPayload = jwtInfo.payload; + } catch (JWT.JWTException e) { + // This should never happen here since we just created/signed the token + throw new ServletException(e); } + + String clientId = accessTokenPayload.get("client_id").getAsString(); + String gid = accessTokenPayload.get("gid").getAsString(); + String jti = accessTokenPayload.get("jti").getAsString(); + + long exp = accessTokenPayload.get("exp").getAsLong(); + + String sessionHandle = null; + if (accessTokenPayload.has("sessionHandle")) { + sessionHandle = accessTokenPayload.get("sessionHandle").getAsString(); + updateLastActive(appIdentifier, sessionHandle); + } + + OAuth.createOrUpdateOauthSession(main, appIdentifier, storage, clientId, gid, null, null, sessionHandle, List.of(jti), exp); } } } @@ -144,7 +158,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I super.sendJsonResponse(200, finalResponse, resp); } - } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | OAuthClientNotFoundException e) { throw new ServletException(e); } } 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 ee6743681..e9565baaf 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -17,8 +17,8 @@ package io.supertokens.webserver.api.oauth; import com.auth0.jwt.exceptions.JWTCreationException; -import com.google.gson.*; - +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import io.supertokens.ActiveUsers; import io.supertokens.Main; import io.supertokens.exceptions.TryRefreshTokenException; @@ -28,6 +28,7 @@ import io.supertokens.oauth.HttpRequestForOAuthProvider; import io.supertokens.oauth.OAuth; import io.supertokens.oauth.OAuthToken; +import io.supertokens.oauth.Transformations; import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; @@ -61,6 +62,7 @@ import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.HashMap; +import java.util.List; import java.util.Map; public class OAuthTokenAPI extends WebserverAPI { @@ -121,10 +123,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String refreshToken = InputParser.parseStringOrThrowError(bodyFromSDK, "refresh_token", false); inputRefreshToken = refreshToken; - String oauthProviderRefreshToken = OAuth.getOAuthProviderRefreshToken(main, appIdentifier, storage, refreshToken); + String internalRefreshToken = OAuth.getInternalRefreshToken(main, appIdentifier, storage, refreshToken); Map formFieldsForTokenIntrospect = new HashMap<>(); - formFieldsForTokenIntrospect.put("token", oauthProviderRefreshToken); + formFieldsForTokenIntrospect.put("token", internalRefreshToken); HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyFormPOST( main, req, resp, @@ -145,7 +147,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject refreshTokenPayload = response.jsonResponse.getAsJsonObject(); try { - OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, refreshTokenPayload, refreshToken); + OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, refreshTokenPayload, refreshToken, oauthClient.clientId); } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { throw new ServletException(e); @@ -159,7 +161,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I return; } - formFields.put("refresh_token", oauthProviderRefreshToken); + formFields.put("refresh_token", internalRefreshToken); } HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyFormPOST( @@ -186,10 +188,29 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } } + String gid = null; + String jti = null; + String sessionHandle = null; + Long accessTokenExp = null; + + if(response.jsonResponse.getAsJsonObject().has("access_token")){ + try { + JsonObject accessTokenPayload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, response.jsonResponse.getAsJsonObject().get("access_token").getAsString()); + gid = accessTokenPayload.get("gid").getAsString(); + jti = accessTokenPayload.get("jti").getAsString(); + accessTokenExp = accessTokenPayload.get("exp").getAsLong(); + if (accessTokenPayload.has("sessionHandle")) { + sessionHandle = accessTokenPayload.get("sessionHandle").getAsString(); + updateLastActive(appIdentifier, sessionHandle); + } + } catch (TryRefreshTokenException e) { + //ignore, shouldn't happen + } + } + if (response.jsonResponse.getAsJsonObject().has("refresh_token")) { String newRefreshToken = response.jsonResponse.getAsJsonObject().get("refresh_token").getAsString(); long refreshTokenExp = 0; - { // Introspect the new refresh token to get the expiry Map formFieldsForTokenIntrospect = new HashMap<>(); @@ -210,41 +231,31 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I if (introspectResponse != null) { JsonObject refreshTokenPayload = introspectResponse.jsonResponse.getAsJsonObject(); refreshTokenExp = refreshTokenPayload.get("exp").getAsLong(); - if (refreshTokenPayload.has("sessionHandle")) { - updateLastActive(appIdentifier, refreshTokenPayload.get("sessionHandle").getAsString()); - } - } else { throw new IllegalStateException("Should never come here"); } } if (inputRefreshToken == null) { - // Issuing a new refresh token - if (!oauthClient.enableRefreshTokenRotation) { - OAuth.createOrUpdateRefreshTokenMapping(main, appIdentifier, storage, newRefreshToken, newRefreshToken, refreshTokenExp); - } // else we don't need a mapping + // Issuing a new refresh token, always creating a mapping. + OAuth.createOrUpdateOauthSession(main, appIdentifier, storage, clientId, gid, newRefreshToken, null, sessionHandle, List.of(jti), refreshTokenExp); } else { // Refreshing a token if (!oauthClient.enableRefreshTokenRotation) { - OAuth.createOrUpdateRefreshTokenMapping(main, appIdentifier, storage, inputRefreshToken, newRefreshToken, refreshTokenExp); + OAuth.createOrUpdateOauthSession(main, appIdentifier, storage, clientId, gid, inputRefreshToken, newRefreshToken, sessionHandle, List.of(jti), refreshTokenExp); response.jsonResponse.getAsJsonObject().remove("refresh_token"); } else { - OAuth.deleteRefreshTokenMappingIfExists(main, appIdentifier, storage, inputRefreshToken); + OAuth.createOrUpdateOauthSession(main, appIdentifier, storage, clientId, gid, newRefreshToken, null, sessionHandle, List.of(jti), refreshTokenExp); } } } else { - try { - JsonObject accessTokenPayload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, response.jsonResponse.getAsJsonObject().get("access_token").getAsString()); - if (accessTokenPayload.has("sessionHandle")) { - updateLastActive(appIdentifier, accessTokenPayload.get("sessionHandle").getAsString()); - } - } catch (Exception e) { - // ignore - } + OAuth.createOrUpdateOauthSession(main, appIdentifier, storage, clientId, gid, null, null, sessionHandle, List.of(jti), accessTokenExp); } - } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { + } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | StorageQueryException + | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException + | JWTCreationException | JWTException | StorageTransactionLogicException + | UnsupportedJWTSigningAlgorithmException | OAuthClientNotFoundException 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 a42aa775d..77cd53efd 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java @@ -16,7 +16,8 @@ package io.supertokens.webserver.api.oauth; -import com.google.gson.*; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; @@ -66,7 +67,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - token = OAuth.getOAuthProviderRefreshToken(main, appIdentifier, storage, token); + token = OAuth.getInternalRefreshToken(main, appIdentifier, storage, token); formFields.put("token", token); HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyFormPOST( @@ -83,9 +84,12 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I if (response != null) { JsonObject finalResponse = response.jsonResponse.getAsJsonObject(); - + String clientId = null; + if(finalResponse.has("client_id")) { + clientId = finalResponse.get("client_id").getAsString(); + } try { - OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, finalResponse, token); + OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, finalResponse, token, clientId); } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException 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 index 322a4ce2d..d958821de 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java @@ -1,12 +1,6 @@ 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; @@ -27,6 +21,11 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + public class RevokeOAuthTokenAPI extends WebserverAPI { public RevokeOAuthTokenAPI(Main main){ super(main, RECIPE_ID.OAUTH.toString()); @@ -47,7 +46,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); if (token.startsWith("st_rt_")) { - token = OAuth.getOAuthProviderRefreshToken(main, appIdentifier, storage, token); + token = OAuth.getInternalRefreshToken(main, appIdentifier, storage, token); String gid = null; long exp = -1; @@ -70,9 +69,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I if (response != null) { JsonObject finalResponse = response.jsonResponse.getAsJsonObject(); + String clientId = null; + if (finalResponse.has("client_id")){ + clientId = finalResponse.get("client_id").getAsString(); + } try { - OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, finalResponse, token); + OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, finalResponse, token, clientId); if (finalResponse.get("active").getAsBoolean()) { gid = finalResponse.get("gid").getAsString(); exp = finalResponse.get("exp").getAsLong(); @@ -128,7 +131,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I // Success response would mean that the clientId/secret has been validated if (gid != null) { try { - OAuth.revokeRefreshToken(main, appIdentifier, storage, gid, exp); + OAuth.revokeRefreshToken(main, appIdentifier, storage, gid); } catch (StorageQueryException | NoSuchAlgorithmException e) { throw new ServletException(e); } diff --git a/src/test/java/io/supertokens/test/oauth/OAuthStorageTest.java b/src/test/java/io/supertokens/test/oauth/OAuthStorageTest.java index 725b91e5e..743362b46 100644 --- a/src/test/java/io/supertokens/test/oauth/OAuthStorageTest.java +++ b/src/test/java/io/supertokens/test/oauth/OAuthStorageTest.java @@ -22,7 +22,6 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.oauth.OAuthClient; import io.supertokens.pluginInterface.oauth.OAuthLogoutChallenge; -import io.supertokens.pluginInterface.oauth.OAuthRevokeTargetType; import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.pluginInterface.oauth.exception.DuplicateOAuthLogoutChallengeException; import io.supertokens.pluginInterface.oauth.exception.OAuthClientNotFoundException; @@ -177,65 +176,33 @@ public void testRevoke() throws Exception { AppIdentifier appIdentifier = new AppIdentifier(null, null); - storage.revokeOAuthTokensBasedOnTargetFields(appIdentifier, OAuthRevokeTargetType.GID, "abcd", System.currentTimeMillis()/1000 + 2 - 3600 * 24 * 31); - storage.revokeOAuthTokensBasedOnTargetFields(appIdentifier, OAuthRevokeTargetType.SESSION_HANDLE, "efgh", System.currentTimeMillis()/1000 + 2 - 3600 * 24 * 31); - storage.revokeOAuthTokensBasedOnTargetFields(appIdentifier, OAuthRevokeTargetType.JTI, "ijkl", System.currentTimeMillis()/1000 + 2 - 3600 * 24 * 31); - - assertTrue(storage.isOAuthTokenRevokedBasedOnTargetFields( - appIdentifier, - new OAuthRevokeTargetType[]{OAuthRevokeTargetType.GID}, - new String[]{"abcd"}, - System.currentTimeMillis()/1000 - 2 - )); - assertFalse(storage.isOAuthTokenRevokedBasedOnTargetFields( - appIdentifier, - new OAuthRevokeTargetType[]{OAuthRevokeTargetType.GID}, - new String[]{"efgh"}, - System.currentTimeMillis()/1000 - 2 - )); - assertTrue(storage.isOAuthTokenRevokedBasedOnTargetFields( - appIdentifier, - new OAuthRevokeTargetType[]{OAuthRevokeTargetType.GID, OAuthRevokeTargetType.SESSION_HANDLE}, - new String[]{"efgh", "efgh"}, - System.currentTimeMillis()/1000 - 2 - )); + storage.addOrUpdateOauthClient(appIdentifier, "clientid", "clientSecret", false, true); + storage.createOrUpdateOAuthSession(appIdentifier, "abcd", "clientid", "externalRefreshToken", + "internalRefreshToken", "efgh", List.of("ijkl", "mnop"), System.currentTimeMillis() + 1000 * 60 * 60 * 24); + + assertFalse(storage.isOAuthTokenRevokedByJTI(appIdentifier, "abcd", "ijkl")); + assertFalse(storage.isOAuthTokenRevokedByJTI(appIdentifier, "abcd", "mnop")); + + storage.revokeOAuthTokenByJTI(appIdentifier, "abcd","ijkl"); + assertTrue(storage.isOAuthTokenRevokedByJTI(appIdentifier, "abcd", "ijkl")); + assertFalse(storage.isOAuthTokenRevokedByJTI(appIdentifier, "abcd", "mnop")); + + storage.revokeOAuthTokenByJTI(appIdentifier, "abcd","mnop"); + assertTrue(storage.isOAuthTokenRevokedByJTI(appIdentifier, "abcd", "ijkl")); + assertTrue(storage.isOAuthTokenRevokedByJTI(appIdentifier, "abcd", "mnop")); + + + storage.revokeOAuthTokenByGID(appIdentifier, "abcd"); + assertTrue(storage.isOAuthTokenRevokedByJTI(appIdentifier, "abcd", "mnop")); + + storage.createOrUpdateOAuthSession(appIdentifier, "abcd", "clientid", "externalRefreshToken", + "internalRefreshToken", "efgh", List.of("ijkl", "mnop"), System.currentTimeMillis() + 1000 * 60 * 60 * 24); + storage.revokeOAuthTokenBySessionHandle(appIdentifier, "efgh"); + assertTrue(storage.isOAuthTokenRevokedByJTI(appIdentifier, "abcd", "mnop")); // test cleanup Thread.sleep(3000); - storage.deleteExpiredRevokedOAuthTokens(System.currentTimeMillis() / 1000 - 3); - - assertFalse(storage.isOAuthTokenRevokedBasedOnTargetFields( - appIdentifier, - new OAuthRevokeTargetType[]{OAuthRevokeTargetType.GID}, - new String[]{"abcd"}, - System.currentTimeMillis()/1000 - 5 - )); - assertFalse(storage.isOAuthTokenRevokedBasedOnTargetFields( - appIdentifier, - new OAuthRevokeTargetType[]{OAuthRevokeTargetType.GID, OAuthRevokeTargetType.SESSION_HANDLE}, - new String[]{"efgh", "efgh"}, - System.currentTimeMillis()/1000 - 5 - )); - - // newly issued should be allowed - storage.revokeOAuthTokensBasedOnTargetFields(appIdentifier, OAuthRevokeTargetType.GID, "abcd", System.currentTimeMillis()/1000 + 2 - 3600 * 24 * 31); - storage.revokeOAuthTokensBasedOnTargetFields(appIdentifier, OAuthRevokeTargetType.SESSION_HANDLE, "efgh", System.currentTimeMillis()/1000 + 2 - 3600 * 24 * 31); - storage.revokeOAuthTokensBasedOnTargetFields(appIdentifier, OAuthRevokeTargetType.JTI, "ijkl", System.currentTimeMillis()/1000 + 2 - 3600 * 24 * 31); - - Thread.sleep(2000); - - assertFalse(storage.isOAuthTokenRevokedBasedOnTargetFields( - appIdentifier, - new OAuthRevokeTargetType[]{OAuthRevokeTargetType.GID}, - new String[]{"abcd"}, - System.currentTimeMillis()/1000 - )); - assertFalse(storage.isOAuthTokenRevokedBasedOnTargetFields( - appIdentifier, - new OAuthRevokeTargetType[]{OAuthRevokeTargetType.GID, OAuthRevokeTargetType.SESSION_HANDLE}, - new String[]{"efgh", "efgh"}, - System.currentTimeMillis()/1000 - )); + storage.deleteExpiredOAuthSessions(System.currentTimeMillis() / 1000 - 3); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -304,8 +271,7 @@ public void testConstraints() throws Exception { // this is what we expect } { - storage.revokeOAuthTokensBasedOnTargetFields(appIdentifier, OAuthRevokeTargetType.GID, "abcd", 0); - storage.revokeOAuthTokensBasedOnTargetFields(appIdentifier, OAuthRevokeTargetType.GID, "abcd", 0); // should update + assertFalse(storage.revokeOAuthTokenByGID(appIdentifier, "abcd")); } // App id FK @@ -316,12 +282,8 @@ public void testConstraints() throws Exception { } catch (TenantOrAppNotFoundException e) { // expected } - try { - storage.revokeOAuthTokensBasedOnTargetFields(appIdentifier2, OAuthRevokeTargetType.GID, "abcd", 0); - fail(); - } catch (TenantOrAppNotFoundException e) { - // expected - } + + assertFalse(storage.revokeOAuthTokenByGID(appIdentifier2, "abcd")); // Client FK try { @@ -349,6 +311,22 @@ public void testConstraints() throws Exception { // expected } + try { + storage.createOrUpdateOAuthSession(appIdentifier2, "abcd", "clientid", null, null, null, List.of("asdasd"), + System.currentTimeMillis() + 10000); + fail(); + } catch (OAuthClientNotFoundException e) { + //expected + } + + try { + storage.createOrUpdateOAuthSession(appIdentifier2, "abcd", "clientid-not-existing", null, null, null, List.of("asdasd"), + System.currentTimeMillis() + 10000); + fail(); + } catch (OAuthClientNotFoundException e) { + //expected + } + process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } diff --git a/src/test/java/io/supertokens/test/oauth/api/TestAuthCodeFlow.java b/src/test/java/io/supertokens/test/oauth/api/TestAuthCodeFlow.java index 228b16ec9..841f69de8 100644 --- a/src/test/java/io/supertokens/test/oauth/api/TestAuthCodeFlow.java +++ b/src/test/java/io/supertokens/test/oauth/api/TestAuthCodeFlow.java @@ -16,6 +16,9 @@ package io.supertokens.test.oauth.api; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import io.supertokens.ProcessState; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlag; @@ -31,12 +34,6 @@ import org.junit.Test; import org.junit.rules.TestRule; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; - -import static org.junit.Assert.assertNotNull; - import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLDecoder; @@ -44,6 +41,8 @@ import java.util.LinkedHashMap; import java.util.Map; +import static org.junit.Assert.assertNotNull; + public class TestAuthCodeFlow { @Rule public TestRule watchman = Utils.getOnFailure(); @@ -172,7 +171,9 @@ public void testAuthCodeGrantFlow() throws Exception { JsonArray audience = new JsonArray(); acceptConsentRequestBody.add("grantAccessTokenAudience", audience); JsonObject session = new JsonObject(); - session.add("access_token", new JsonObject()); + JsonObject accessToken = new JsonObject(); + accessToken.addProperty("gid", "gidForTesting"); + session.add("access_token", accessToken); session.add("id_token", new JsonObject()); acceptConsentRequestBody.add("session", session); diff --git a/src/test/java/io/supertokens/test/oauth/api/TestIssueTokens.java b/src/test/java/io/supertokens/test/oauth/api/TestIssueTokens.java new file mode 100644 index 000000000..2484ea4ee --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/TestIssueTokens.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.oauth.api; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.session.Session; +import io.supertokens.session.info.SessionInformationHolder; +import io.supertokens.session.jwt.JWT; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class TestIssueTokens { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + OAuthAPIHelper.resetOAuthProvider(); + } + + @Test + public void testAccessToken() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("oauth_provider_public_service_url", "http://localhost:4444"); + Utils.setValueInConfig("oauth_provider_admin_service_url", "http://localhost:4445"); + Utils.setValueInConfig("oauth_provider_consent_login_base_url", "http://localhost:3001/auth"); + Utils.setValueInConfig("oauth_client_secret_encryption_key", "secret"); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { EE_FEATURES.OAUTH }); + + JsonObject client = createClient(process.getProcess()); + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password123"); + SessionInformationHolder session = Session.createNewSession(process.getProcess(), user.getSupertokensUserId(), + new JsonObject(), new JsonObject()); + + JsonObject tokenResponse = issueTokens(process.getProcess(), client, user.getSupertokensUserId(), + user.getSupertokensUserId(), session.session.handle); + + String accessToken = tokenResponse.get("access_token").getAsString(); + JWT.JWTInfo accessTokenInfo = JWT.getPayloadWithoutVerifying(accessToken); + assertTrue(accessTokenInfo.payload.has("iss")); + assertEquals("http://localhost:3001/auth", accessTokenInfo.payload.get("iss").getAsString()); + + String idToken = tokenResponse.get("id_token").getAsString(); + JWT.JWTInfo idTokenInfo = JWT.getPayloadWithoutVerifying(idToken); + assertTrue(idTokenInfo.payload.has("iss")); + assertEquals("http://localhost:3001/auth", idTokenInfo.payload.get("iss").getAsString()); + + // test introspect access token + JsonObject introspectResponse = introspectToken(process.getProcess(), + tokenResponse.get("access_token").getAsString()); + assertEquals("OK", introspectResponse.get("status").getAsString()); + assertTrue(introspectResponse.get("active").getAsBoolean()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private JsonObject issueTokens(Main main, JsonObject client, String sub, String rsub, String sessionHandle) + throws Exception { + JsonObject authRequestBody = new JsonObject(); + JsonObject params = new JsonObject(); + params.addProperty("client_id", client.get("clientId").getAsString()); + params.addProperty("redirect_uri", "http://localhost.com:3000/auth/callback/supertokens"); + params.addProperty("response_type", "code"); + params.addProperty("scope", "openid offline_access"); + params.addProperty("state", "test12345678"); + + authRequestBody.add("params", params); + + JsonObject authResponse = OAuthAPIHelper.auth(main, authRequestBody); + String cookies = authResponse.get("cookies").getAsJsonArray().get(0).getAsString(); + cookies = cookies.split(";")[0]; + + String redirectTo = authResponse.get("redirectTo").getAsString(); + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001/auth"); + + URL url = new URL(redirectTo); + Map queryParams = splitQuery(url); + String loginChallenge = queryParams.get("login_challenge"); + + Map acceptLoginRequestParams = new HashMap<>(); + acceptLoginRequestParams.put("loginChallenge", loginChallenge); + + JsonObject acceptLoginRequestBody = new JsonObject(); + acceptLoginRequestBody.addProperty("subject", sub); + acceptLoginRequestBody.addProperty("remember", true); + acceptLoginRequestBody.addProperty("rememberFor", 3600); + acceptLoginRequestBody.addProperty("identityProviderSessionId", sessionHandle); + + JsonObject acceptLoginRequestResponse = OAuthAPIHelper.acceptLoginRequest(main, acceptLoginRequestParams, + acceptLoginRequestBody); + + redirectTo = acceptLoginRequestResponse.get("redirectTo").getAsString(); + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001/auth"); + + url = new URL(redirectTo); + queryParams = splitQuery(url); + + params = new JsonObject(); + for (Map.Entry entry : queryParams.entrySet()) { + params.addProperty(entry.getKey(), entry.getValue()); + } + authRequestBody.add("params", params); + authRequestBody.addProperty("cookies", cookies); + + authResponse = OAuthAPIHelper.auth(main, authRequestBody); + + redirectTo = authResponse.get("redirectTo").getAsString(); + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001/auth"); + cookies = authResponse.get("cookies").getAsJsonArray().get(0).getAsString(); + cookies = cookies.split(";")[0]; + + url = new URL(redirectTo); + queryParams = splitQuery(url); + + String consentChallenge = queryParams.get("consent_challenge"); + + JsonObject acceptConsentRequestBody = new JsonObject(); + acceptConsentRequestBody.addProperty("iss", "http://localhost:3001/auth"); + acceptConsentRequestBody.addProperty("tId", "public"); + acceptConsentRequestBody.addProperty("rsub", rsub); + acceptConsentRequestBody.addProperty("sessionHandle", sessionHandle); + acceptConsentRequestBody.add("initialAccessTokenPayload", new JsonObject()); + acceptConsentRequestBody.add("initialIdTokenPayload", new JsonObject()); + JsonArray grantScope = new JsonArray(); + grantScope.add(new JsonPrimitive("openid")); + grantScope.add(new JsonPrimitive("offline_access")); + acceptConsentRequestBody.add("grantScope", grantScope); + JsonArray audience = new JsonArray(); + acceptConsentRequestBody.add("grantAccessTokenAudience", audience); + JsonObject session = new JsonObject(); + session.add("access_token", new JsonObject()); + session.add("id_token", new JsonObject()); + acceptConsentRequestBody.add("session", session); + + queryParams = new HashMap<>(); + queryParams.put("consentChallenge", consentChallenge); + + JsonObject acceptConsentRequestResponse = OAuthAPIHelper.acceptConsentRequest(main, queryParams, + acceptConsentRequestBody); + + redirectTo = acceptConsentRequestResponse.get("redirectTo").getAsString(); + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001/auth"); + + url = new URL(redirectTo); + queryParams = splitQuery(url); + + params = new JsonObject(); + for (Map.Entry entry : queryParams.entrySet()) { + params.addProperty(entry.getKey(), entry.getValue()); + } + authRequestBody.add("params", params); + authRequestBody.addProperty("cookies", cookies); + + authResponse = OAuthAPIHelper.auth(main, authRequestBody); + + redirectTo = authResponse.get("redirectTo").getAsString(); + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001/auth"); + + url = new URL(redirectTo); + queryParams = splitQuery(url); + + String authorizationCode = queryParams.get("code"); + + JsonObject tokenRequestBody = new JsonObject(); + JsonObject inputBody = new JsonObject(); + inputBody.addProperty("grant_type", "authorization_code"); + inputBody.addProperty("code", authorizationCode); + inputBody.addProperty("redirect_uri", "http://localhost.com:3000/auth/callback/supertokens"); + inputBody.addProperty("client_id", client.get("clientId").getAsString()); + inputBody.addProperty("client_secret", client.get("clientSecret").getAsString()); + tokenRequestBody.add("inputBody", inputBody); + tokenRequestBody.addProperty("iss", "http://localhost:3001/auth"); + + JsonObject tokenResponse = OAuthAPIHelper.token(main, tokenRequestBody); + return tokenResponse; + } + + private static Map splitQuery(URL url) throws UnsupportedEncodingException { + Map queryPairs = new LinkedHashMap<>(); + String query = url.getQuery(); + String[] pairs = query.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + queryPairs.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), + URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); + } + return queryPairs; + } + + private JsonObject refreshToken(Main main, JsonObject client, String refreshToken) throws Exception { + JsonObject inputBody = new JsonObject(); + inputBody.addProperty("grant_type", "refresh_token"); + inputBody.addProperty("refresh_token", refreshToken); + inputBody.addProperty("client_id", client.get("clientId").getAsString()); + inputBody.addProperty("client_secret", client.get("clientSecret").getAsString()); + + JsonObject tokenBody = new JsonObject(); + tokenBody.add("inputBody", inputBody); + tokenBody.addProperty("iss", "http://localhost:3001/auth"); + tokenBody.add("access_token", new JsonObject()); + tokenBody.add("id_token", new JsonObject()); + return OAuthAPIHelper.token(main, tokenBody); + } + + private JsonObject introspectToken(Main main, String token) throws Exception { + JsonObject introspectRequestBody = new JsonObject(); + introspectRequestBody.addProperty("token", token); + return OAuthAPIHelper.introspect(main, introspectRequestBody); + } + + private JsonObject createClient(Main main) throws Exception { + JsonObject clientBody = new JsonObject(); + JsonArray grantTypes = new JsonArray(); + grantTypes.add(new JsonPrimitive("authorization_code")); + grantTypes.add(new JsonPrimitive("refresh_token")); + clientBody.add("grantTypes", grantTypes); + JsonArray responseTypes = new JsonArray(); + responseTypes.add(new JsonPrimitive("code")); + responseTypes.add(new JsonPrimitive("id_token")); + clientBody.add("responseTypes", responseTypes); + JsonArray redirectUris = new JsonArray(); + redirectUris.add(new JsonPrimitive("http://localhost.com:3000/auth/callback/supertokens")); + clientBody.add("redirectUris", redirectUris); + clientBody.addProperty("scope", "openid email offline_access"); + clientBody.addProperty("tokenEndpointAuthMethod", "client_secret_post"); + + JsonObject client = OAuthAPIHelper.createClient(main, clientBody); + return client; + } +} diff --git a/src/test/java/io/supertokens/test/oauth/api/TestRefreshTokenFlowWithTokenRotationOptions.java b/src/test/java/io/supertokens/test/oauth/api/TestRefreshTokenFlowWithTokenRotationOptions.java index f79035649..967862d1b 100644 --- a/src/test/java/io/supertokens/test/oauth/api/TestRefreshTokenFlowWithTokenRotationOptions.java +++ b/src/test/java/io/supertokens/test/oauth/api/TestRefreshTokenFlowWithTokenRotationOptions.java @@ -16,6 +16,9 @@ package io.supertokens.test.oauth.api; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import io.supertokens.Main; import io.supertokens.ProcessState; import io.supertokens.featureflag.EE_FEATURES; @@ -32,10 +35,6 @@ import org.junit.Test; import org.junit.rules.TestRule; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; - import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLDecoder; @@ -95,6 +94,7 @@ private static JsonObject completeFlowAndGetTokens(Main main, JsonObject client) params.addProperty("response_type", "code"); params.addProperty("scope", "openid offline_access"); params.addProperty("state", "test12345678"); + authRequestBody.add("params", params); JsonObject authResponse = OAuthAPIHelper.auth(main, authRequestBody); @@ -158,6 +158,8 @@ private static JsonObject completeFlowAndGetTokens(Main main, JsonObject client) JsonArray audience = new JsonArray(); acceptConsentRequestBody.add("grantAccessTokenAudience", audience); JsonObject session = new JsonObject(); +// JsonObject accessToken = new JsonObject(); +// accessToken.addProperty("gid", "gidForTesting"); session.add("access_token", new JsonObject()); session.add("id_token", new JsonObject()); acceptConsentRequestBody.add("session", session); @@ -350,6 +352,7 @@ public void testRefreshTokenWhenRotationIsEnabledAfter() throws Exception { assertEquals("OAUTH_ERROR", newTokens.get("status").getAsString()); assertEquals("token_inactive", newTokens.get("error").getAsString()); + newTokens = refreshToken(process.getProcess(), client, newRefreshToken); assertTrue(newTokens.has("refresh_token")); @@ -399,6 +402,12 @@ public void testRefreshTokenWithRotationIsDisabledAfter() throws Exception { newTokens = refreshToken(process.getProcess(), client, newRefreshToken); assertFalse(newTokens.has("refresh_token")); + newTokens = refreshToken(process.getProcess(), client, newRefreshToken); + assertFalse(newTokens.has("refresh_token")); + + newTokens = refreshToken(process.getProcess(), client, newRefreshToken); + assertFalse(newTokens.has("refresh_token")); + process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } diff --git a/src/test/java/io/supertokens/test/oauth/api/TestRevoke5_2.java b/src/test/java/io/supertokens/test/oauth/api/TestRevoke5_2.java index 66e23548f..c0eece257 100644 --- a/src/test/java/io/supertokens/test/oauth/api/TestRevoke5_2.java +++ b/src/test/java/io/supertokens/test/oauth/api/TestRevoke5_2.java @@ -1,27 +1,8 @@ package io.supertokens.test.oauth.api; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import java.io.UnsupportedEncodingException; -import java.net.URL; -import java.net.URLDecoder; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestRule; - import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; - import io.supertokens.Main; import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; @@ -34,6 +15,20 @@ import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.*; public class TestRevoke5_2 { @Rule @@ -180,26 +175,25 @@ public void testRevokeClientId() throws Exception { // revoke client id JsonObject revokeClientIdResponse = revokeClientId(process.getProcess(), client); - System.out.println(revokeClientIdResponse.toString()); assertEquals("OK", revokeClientIdResponse.get("status").getAsString()); Thread.sleep(1000); - // test introspect refresh token (allowed) + // test introspect refresh token (should be revoked also - not allowed) JsonObject introspectResponse = introspectToken(process.getProcess(), tokenResponse.get("refresh_token").getAsString()); assertEquals("OK", introspectResponse.get("status").getAsString()); - assertTrue(introspectResponse.get("active").getAsBoolean()); + assertFalse(introspectResponse.get("active").getAsBoolean()); // test introspect access token (not allowed) introspectResponse = introspectToken(process.getProcess(), tokenResponse.get("access_token").getAsString()); assertEquals("OK", introspectResponse.get("status").getAsString()); assertFalse(introspectResponse.get("active").getAsBoolean()); - // test refresh token (allowed) + // test refresh token (not allowed) JsonObject refreshResponse = refreshToken(process.getProcess(), client, tokenResponse.get("refresh_token").getAsString()); - assertEquals("OK", refreshResponse.get("status").getAsString()); + assertEquals("OAUTH_ERROR", refreshResponse.get("status").getAsString()); Thread.sleep(1000);