diff --git a/CHANGELOG.md b/CHANGELOG.md index aa6e09e73..46a4d369d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,28 +47,31 @@ If using PostgreSQL, run the following SQL script: ```sql CREATE TABLE IF NOT EXISTS oauth_clients ( app_id VARCHAR(64), - client_id VARCHAR(128) NOT NULL, + client_id VARCHAR(255) NOT NULL, is_client_credentials_only BOOLEAN NOT NULL, PRIMARY KEY (app_id, client_id), 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', - client_id VARCHAR(128) NOT NULL, + client_id VARCHAR(255) NOT NULL, iat BIGINT NOT NULL, exp BIGINT NOT NULL, PRIMARY KEY (app_id, client_id, iat), @@ -81,7 +84,7 @@ CREATE INDEX IF NOT EXISTS oauth_m2m_token_exp_index ON oauth_m2m_tokens(exp DES CREATE TABLE IF NOT EXISTS oauth_logout_challenges ( app_id VARCHAR(64) DEFAULT 'public', challenge VARCHAR(128) NOT NULL, - client_id VARCHAR(128) NOT NULL, + client_id VARCHAR(255) NOT NULL, post_logout_redirect_uri VARCHAR(1024), session_handle VARCHAR(128), state VARCHAR(128), @@ -98,28 +101,32 @@ If using MySQL, run the following SQL script: ```sql CREATE TABLE IF NOT EXISTS oauth_clients ( app_id VARCHAR(64), - client_id VARCHAR(128) NOT NULL, + client_id VARCHAR(255) NOT NULL, is_client_credentials_only BOOLEAN NOT NULL, PRIMARY KEY (app_id, client_id), 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', - client_id VARCHAR(128) NOT NULL, + client_id VARCHAR(255) NOT NULL, iat BIGINT UNSIGNED NOT NULL, exp BIGINT UNSIGNED NOT NULL, PRIMARY KEY (app_id, client_id, iat), @@ -132,7 +139,7 @@ CREATE INDEX oauth_m2m_token_exp_index ON oauth_m2m_tokens(exp DESC); CREATE TABLE IF NOT EXISTS oauth_logout_challenges ( app_id VARCHAR(64) DEFAULT 'public', challenge VARCHAR(128) NOT NULL, - client_id VARCHAR(128) NOT NULL, + client_id VARCHAR(255) NOT NULL, post_logout_redirect_uri VARCHAR(1024), session_handle VARCHAR(128), state VARCHAR(128), diff --git a/config.yaml b/config.yaml index aed18f4cb..5f6a8f80f 100644 --- a/config.yaml +++ b/config.yaml @@ -167,3 +167,6 @@ core_config_version: 0 # (OPTIONAL | Default: oauth_provider_public_service_url) If specified, the core uses this URL to parse responses from # the oauth provider when the oauth provider's internal address differs from the known public provider address. # oauth_provider_url_configured_in_oauth_provider: + +# (Optional | Default: null) string value. The encryption key used for saving OAuth client secret on the database. +# oauth_client_secret_encryption_key: diff --git a/devConfig.yaml b/devConfig.yaml index acc443030..9557ada23 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -167,3 +167,6 @@ disable_telemetry: true # (OPTIONAL | Default: oauth_provider_public_service_url) If specified, the core uses this URL to parse responses from # the oauth provider when the oauth provider's internal address differs from the known public provider address. # oauth_provider_url_configured_in_oauth_provider: + +# (Optional | Default: null) string value. The encryption key used for saving OAuth client secret on the database. +# oauth_client_secret_encryption_key: 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/config/CoreConfig.java b/src/main/java/io/supertokens/config/CoreConfig.java index baf09a609..e8c266fe3 100644 --- a/src/main/java/io/supertokens/config/CoreConfig.java +++ b/src/main/java/io/supertokens/config/CoreConfig.java @@ -307,6 +307,12 @@ public class CoreConfig { "If specified, the core uses this URL to parse responses from the oauth provider when the oauth provider's internal address differs from the known public provider address.") private String oauth_provider_url_configured_in_oauth_provider = null; + @ConfigYamlOnly + @JsonProperty + @HideFromDashboard + @ConfigDescription("The encryption key used for saving OAuth client secret on the database.") + private String oauth_client_secret_encryption_key = null; + @ConfigYamlOnly @JsonProperty @ConfigDescription( @@ -391,6 +397,13 @@ public String getOAuthProviderUrlConfiguredInOAuthProvider() throws InvalidConfi return oauth_provider_url_configured_in_oauth_provider; } + public String getOAuthClientSecretEncryptionKey() throws InvalidConfigException { + if(oauth_client_secret_encryption_key == null) { + throw new InvalidConfigException("oauth_client_secret_encryption_key is not set"); + } + return oauth_client_secret_encryption_key; + } + public String getIpAllowRegex() { return ip_allow_regex; } diff --git a/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeListAndChallenges/CleanupOAuthRevokeListAndChallenges.java b/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeListAndChallenges/CleanupOAuthSessionsAndChallenges.java similarity index 70% rename from src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeListAndChallenges/CleanupOAuthRevokeListAndChallenges.java rename to src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeListAndChallenges/CleanupOAuthSessionsAndChallenges.java index 89f0f8c67..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 @@ -33,8 +33,11 @@ protected void doTaskPerStorage(Storage storage) throws Exception { } OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - oauthStorage.cleanUpExpiredAndRevokedOAuthTokensList(); - oauthStorage.deleteOAuthLogoutChallengesBefore(System.currentTimeMillis() - 1000 * 60 * 60 * 48); + long monthAgo = System.currentTimeMillis() / 1000 - 31 * 24 * 3600; + oauthStorage.deleteExpiredOAuthSessions(monthAgo); + oauthStorage.deleteExpiredOAuthM2MTokens(monthAgo); + + oauthStorage.deleteOAuthLogoutChallengesBefore(System.currentTimeMillis() - 1000 * 60 * 60 * 48); // 48 hours } @Override diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 30ca38649..06684ce17 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -49,14 +49,17 @@ 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; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; 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; @@ -3015,20 +3018,24 @@ public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(A } @Override - public boolean doesOAuthClientIdExist(AppIdentifier appIdentifier, String clientId) - throws StorageQueryException { + public OAuthClient getOAuthClientById(AppIdentifier appIdentifier, String clientId) + throws StorageQueryException, OAuthClientNotFoundException { try { - return OAuthQueries.doesOAuthClientIdExist(this, clientId, appIdentifier); + OAuthClient client = OAuthQueries.getOAuthClientById(this, clientId, appIdentifier); + if (client == null) { + throw new OAuthClientNotFoundException(); + } + return client; } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public void addOrUpdateOauthClient(AppIdentifier appIdentifier, String clientId, boolean isClientCredentialsOnly) + public void addOrUpdateOauthClient(AppIdentifier appIdentifier, String clientId, String clientSecret, boolean isClientCredentialsOnly, boolean enableRefreshTokenRotation) throws StorageQueryException, TenantOrAppNotFoundException { try { - OAuthQueries.addOrUpdateOauthClient(this, appIdentifier, clientId, isClientCredentialsOnly); + OAuthQueries.addOrUpdateOauthClient(this, appIdentifier, clientId, clientSecret, isClientCredentialsOnly, enableRefreshTokenRotation); } catch (SQLException e) { if (e instanceof SQLiteException) { String errorMessage = e.getMessage(); @@ -3056,42 +3063,48 @@ public boolean deleteOAuthClient(AppIdentifier appIdentifier, String clientId) t } @Override - public List listOAuthClients(AppIdentifier appIdentifier) throws StorageQueryException { + public List getOAuthClients(AppIdentifier appIdentifier, List clientIds) throws StorageQueryException { try { - return OAuthQueries.listOAuthClients(this, appIdentifier); + return OAuthQueries.getOAuthClients(this, appIdentifier, clientIds); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public void revokeOAuthTokensBasedOnTargetFields(AppIdentifier appIdentifier, OAuthRevokeTargetType targetType, String targetValue, long exp) - throws StorageQueryException, TenantOrAppNotFoundException { + public boolean revokeOAuthTokenByGID(AppIdentifier appIdentifier, String gid) throws StorageQueryException { try { - OAuthQueries.revokeOAuthTokensBasedOnTargetFields(this, appIdentifier, targetType, targetValue, exp); + return OAuthQueries.deleteOAuthSessionByGID(this, appIdentifier, gid); } catch (SQLException e) { - if (e instanceof SQLiteException) { - String errorMessage = e.getMessage(); - SQLiteConfig config = Config.getConfig(this); + throw new StorageQueryException(e); + } + } - if (isForeignKeyConstraintError( - errorMessage, - config.getOAuthRevokeTable(), - new String[]{"app_id"}, - new Object[]{appIdentifier.getAppId()})) { - throw new TenantOrAppNotFoundException(appIdentifier); - } - } + @Override + public boolean revokeOAuthTokenByClientId(AppIdentifier appIdentifier, String clientId) + throws StorageQueryException { + try { + return OAuthQueries.deleteOAuthSessionByClientId(this, appIdentifier, clientId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public boolean revokeOAuthTokenByJTI(AppIdentifier appIdentifier, String gid, String jti) + throws StorageQueryException { + try { + return OAuthQueries.deleteJTIFromOAuthSession(this, appIdentifier, gid, jti); + } catch (SQLException e) { throw new StorageQueryException(e); } - } @Override - public boolean isOAuthTokenRevokedBasedOnTargetFields(AppIdentifier appIdentifier, OAuthRevokeTargetType[] targetTypes, String[] targetValues, long issuedAt) + public boolean revokeOAuthTokenBySessionHandle(AppIdentifier appIdentifier, String sessionHandle) throws StorageQueryException { try { - return OAuthQueries.isOAuthTokenRevokedBasedOnTargetFields(this, appIdentifier, targetTypes, targetValues, issuedAt); + return OAuthQueries.deleteOAuthSessionBySessionHandle(this, appIdentifier, sessionHandle); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -3120,9 +3133,9 @@ public void addOAuthM2MTokenForStats(AppIdentifier appIdentifier, String clientI } @Override - public void cleanUpExpiredAndRevokedOAuthTokensList() throws StorageQueryException { + public void deleteExpiredOAuthM2MTokens(long exp) throws StorageQueryException { try { - OAuthQueries.cleanUpExpiredAndRevokedOAuthTokensList(this); + OAuthQueries.deleteExpiredOAuthM2MTokens(this, exp); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -3179,6 +3192,50 @@ public void deleteOAuthLogoutChallengesBefore(long time) throws StorageQueryExce } } + @Override + public void createOrUpdateOAuthSession(AppIdentifier appIdentifier, String gid, String clientId, + String externalRefreshToken, String internalRefreshToken, + String sessionHandle, List jtis, long exp) + throws StorageQueryException, OAuthClientNotFoundException { + try { + OAuthQueries.createOrUpdateOAuthSession(this, appIdentifier, gid, clientId, externalRefreshToken, + internalRefreshToken, sessionHandle, jtis, exp); + } catch (SQLException e) { + if (e instanceof SQLiteException) { + String errorMessage = e.getMessage(); + SQLiteConfig config = Config.getConfig(this); + + 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 String getRefreshTokenMapping(AppIdentifier appIdentifier, String externalRefreshToken) + throws StorageQueryException { + try { + return OAuthQueries.getRefreshTokenMapping(this, appIdentifier, externalRefreshToken); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void deleteExpiredOAuthSessions(long exp) throws StorageQueryException { + try { + OAuthQueries.deleteExpiredOAuthSessions(this, exp); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public int countTotalNumberOfOAuthClients(AppIdentifier appIdentifier) throws StorageQueryException { try { @@ -3216,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 75b98b3d5..0790898dc 100644 --- a/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java +++ b/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java @@ -169,14 +169,18 @@ public String getOAuthClientsTable() { return "oauth_clients"; } - public String getOAuthRevokeTable() { - return "oauth_revoke"; + public String getOAuthRefreshTokenMappingTable() { + return "oauth_refresh_token_mapping"; } 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 7d55f1634..eb2fe4809 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java @@ -432,13 +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).getOAuthRevokeTable())) { + if (!doesTableExists(start, Config.getConfig(start).getOAuthSessionsTable())) { getInstance(main).addState(CREATING_NEW_TABLE, null); - update(start, OAuthQueries.getQueryToCreateOAuthRevokeTable(start), NO_OP_SETTER); + update(start, OAuthQueries.getQueryToCreateOAuthSessionsTable(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 3c854acd6..f68e2efed 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java @@ -17,16 +17,19 @@ package io.supertokens.inmemorydb.queries; import io.supertokens.inmemorydb.Start; +import io.supertokens.inmemorydb.Utils; import io.supertokens.inmemorydb.config.Config; import io.supertokens.pluginInterface.exceptions.StorageQueryException; 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; @@ -38,39 +41,42 @@ public static String getQueryToCreateOAuthClientTable(Start start) { // @formatter:off return "CREATE TABLE IF NOT EXISTS " + oAuth2ClientTable + " (" + "app_id VARCHAR(64)," - + "client_id VARCHAR(128) NOT NULL," + + "client_id VARCHAR(255) NOT NULL," + + "client_secret TEXT," + + "enable_refresh_token_rotation BOOLEAN 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(); + public static String getQueryToCreateOAuthSessionsTable(Start start) { + String oAuthSessionsTable = Config.getConfig(start).getOAuthSessionsTable(); // @formatter:off - return "CREATE TABLE IF NOT EXISTS " + oAuth2RevokeTable + " (" + 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'," - + "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) { @@ -78,7 +84,7 @@ public static String getQueryToCreateOAuthM2MTokensTable(Start start) { // @formatter:off return "CREATE TABLE IF NOT EXISTS " + oAuth2M2MTokensTable + " (" + "app_id VARCHAR(64) DEFAULT 'public'," - + "client_id VARCHAR(128) NOT NULL," + + "client_id VARCHAR(255) NOT NULL," + "iat BIGINT NOT NULL," + "exp BIGINT NOT NULL," + "PRIMARY KEY (app_id, client_id, iat)," @@ -106,7 +112,7 @@ public static String getQueryToCreateOAuthLogoutChallengesTable(Start start) { return "CREATE TABLE IF NOT EXISTS " + oAuth2LogoutChallengesTable + " (" + "app_id VARCHAR(64) DEFAULT 'public'," + "challenge VARCHAR(128) NOT NULL," - + "client_id VARCHAR(128) NOT NULL," + + "client_id VARCHAR(255) NOT NULL," + "post_logout_redirect_uri VARCHAR(1024)," + "session_handle VARCHAR(128)," + "state VARCHAR(128)," @@ -124,43 +130,85 @@ public static String getQueryToCreateOAuthLogoutChallengesTimeCreatedIndex(Start + oAuth2LogoutChallengesTable + "(time_created DESC);"; } - public static boolean doesOAuthClientIdExist(Start start, String clientId, AppIdentifier appIdentifier) + public static OAuthClient getOAuthClientById(Start start, String clientId, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthClientsTable() + + String QUERY = "SELECT client_secret, is_client_credentials_only, enable_refresh_token_rotation FROM " + Config.getConfig(start).getOAuthClientsTable() + " WHERE client_id = ? AND app_id = ?"; return execute(start, QUERY, pst -> { pst.setString(1, clientId); pst.setString(2, appIdentifier.getAppId()); - }, ResultSet::next); + }, (result) -> { + if (result.next()) { + return new OAuthClient(clientId, result.getString("client_secret"), result.getBoolean("is_client_credentials_only"), result.getBoolean("enable_refresh_token_rotation")); + } + return null; + }); + } + + 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 listOAuthClients(Start start, AppIdentifier appIdentifier) + public static List getOAuthClients(Start start, AppIdentifier appIdentifier, List clientIds) throws SQLException, StorageQueryException { - String QUERY = "SELECT client_id FROM " + Config.getConfig(start).getOAuthClientsTable() + - " WHERE app_id = ?"; + String QUERY = "SELECT * FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE app_id = ? AND client_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(clientIds.size()) + + ")"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); + for (int i = 0; i < clientIds.size(); i++) { + pst.setString(i + 2, clientIds.get(i)); + } }, (result) -> { - List res = new ArrayList<>(); + List res = new ArrayList<>(); while (result.next()) { - res.add(result.getString("client_id")); + res.add(new OAuthClient(result.getString("client_id"), result.getString("client_secret"), result.getBoolean("is_client_credentials_only"), result.getBoolean("enable_refresh_token_rotation"))); } return res; }); } - public static void addOrUpdateOauthClient(Start start, AppIdentifier appIdentifier, String clientId, - boolean isClientCredentialsOnly) + public static void addOrUpdateOauthClient(Start start, AppIdentifier appIdentifier, String clientId, String clientSecret, + boolean isClientCredentialsOnly, boolean enableRefreshTokenRotation) throws SQLException, StorageQueryException { 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 = ?"; + + "(app_id, client_id, client_secret, is_client_credentials_only, enable_refresh_token_rotation) VALUES(?, ?, ?, ?, ?) " + + "ON CONFLICT (app_id, client_id) DO UPDATE SET client_secret = ?, is_client_credentials_only = ?, enable_refresh_token_rotation = ?"; update(start, INSERT, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, clientId); - pst.setBoolean(3, isClientCredentialsOnly); + pst.setString(3, clientSecret); pst.setBoolean(4, isClientCredentialsOnly); + pst.setBoolean(5, enableRefreshTokenRotation); + pst.setString(6, clientSecret); + pst.setBoolean(7, isClientCredentialsOnly); + pst.setBoolean(8, enableRefreshTokenRotation); }); } @@ -175,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, @@ -292,30 +340,6 @@ public static void addOAuthM2MTokenForStats(Start start, AppIdentifier appIdenti }); } - public static void cleanUpExpiredAndRevokedOAuthTokensList(Start start) throws SQLException, StorageQueryException { - { - // delete expired M2M tokens - String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + - " WHERE exp < ?"; - - long timestamp = System.currentTimeMillis() / 1000 - 3600 * 24 * 31; // expired 31 days ago - update(start, QUERY, pst -> { - pst.setLong(1, timestamp); - }); - } - - { - // delete expired revoked tokens - String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthRevokeTable() + - " WHERE exp < ?"; - - long timestamp = System.currentTimeMillis() / 1000 - 3600 * 24 * 31; // expired 31 days ago - update(start, QUERY, pst -> { - pst.setLong(1, timestamp); - }); - } - } - 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() + @@ -370,4 +394,70 @@ public static void deleteOAuthLogoutChallengesBefore(Start start, long time) thr pst.setLong(1, time); }); } + + 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, externalRefreshToken); + }, result -> { + if (result.next()) { + return result.getString("internal_refresh_token"); + } + return null; + }); + } + + 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.setLong(1, exp); + }); + } + + public static void deleteExpiredOAuthM2MTokens(Start start, long exp) throws SQLException, StorageQueryException { + // delete expired M2M tokens + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " 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/HttpRequestForOry.java b/src/main/java/io/supertokens/oauth/HttpRequestForOAuthProvider.java similarity index 99% rename from src/main/java/io/supertokens/oauth/HttpRequestForOry.java rename to src/main/java/io/supertokens/oauth/HttpRequestForOAuthProvider.java index 2b9997f1d..67664c717 100644 --- a/src/main/java/io/supertokens/oauth/HttpRequestForOry.java +++ b/src/main/java/io/supertokens/oauth/HttpRequestForOAuthProvider.java @@ -15,7 +15,7 @@ import java.util.Map; import java.util.stream.Collectors; -public class HttpRequestForOry { +public class HttpRequestForOAuthProvider { // This is a helper class to make HTTP requests to the hydra server specifically. // Although this is similar to HttpRequest, this is slightly modified to be able to work with // form data, headers in request and responses, query params in non-get requests, reading responses in diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index b357588b5..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,24 +27,29 @@ 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; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -68,7 +72,7 @@ private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) "feature."); } - public static HttpRequestForOry.Response doOAuthProxyGET(Main main, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequestForOAuthProvider.Response doOAuthProxyGET(Main main, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -77,9 +81,7 @@ public static HttpRequestForOry.Response doOAuthProxyGET(Main main, AppIdentifie } if (clientIdToCheck != null) { - if (!oauthStorage.doesOAuthClientIdExist(appIdentifier, clientIdToCheck)) { - throw new OAuthClientNotFoundException(); - } + oauthStorage.getOAuthClientById(appIdentifier, clientIdToCheck); // may throw OAuthClientNotFoundException } // Request transformations @@ -93,7 +95,7 @@ public static HttpRequestForOry.Response doOAuthProxyGET(Main main, AppIdentifie } String fullUrl = baseURL + path; - HttpRequestForOry.Response response = HttpRequestForOry.doGet(fullUrl, headers, queryParams); + HttpRequestForOAuthProvider.Response response = HttpRequestForOAuthProvider.doGet(fullUrl, headers, queryParams); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); @@ -109,7 +111,7 @@ public static HttpRequestForOry.Response doOAuthProxyGET(Main main, AppIdentifie return response; } - public static HttpRequestForOry.Response doOAuthProxyFormPOST(Main main, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map formFields, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequestForOAuthProvider.Response doOAuthProxyFormPOST(Main main, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map formFields, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -118,9 +120,7 @@ public static HttpRequestForOry.Response doOAuthProxyFormPOST(Main main, AppIden } if (clientIdToCheck != null) { - if (!oauthStorage.doesOAuthClientIdExist(appIdentifier, clientIdToCheck)) { - throw new OAuthClientNotFoundException(); - } + oauthStorage.getOAuthClientById(appIdentifier, clientIdToCheck); // may throw OAuthClientNotFoundException } // Request transformations @@ -135,7 +135,7 @@ public static HttpRequestForOry.Response doOAuthProxyFormPOST(Main main, AppIden } String fullUrl = baseURL + path; - HttpRequestForOry.Response response = HttpRequestForOry.doFormPost(fullUrl, headers, formFields); + HttpRequestForOAuthProvider.Response response = HttpRequestForOAuthProvider.doFormPost(fullUrl, headers, formFields); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); @@ -150,7 +150,7 @@ public static HttpRequestForOry.Response doOAuthProxyFormPOST(Main main, AppIden return response; } - public static HttpRequestForOry.Response doOAuthProxyJsonPOST(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 HttpRequestForOAuthProvider.Response doOAuthProxyJsonPOST(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 { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -159,9 +159,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPOST(Main main, AppIden } if (clientIdToCheck != null) { - if (!oauthStorage.doesOAuthClientIdExist(appIdentifier, clientIdToCheck)) { - throw new OAuthClientNotFoundException(); - } + oauthStorage.getOAuthClientById(appIdentifier, clientIdToCheck); // may throw OAuthClientNotFoundException } // Request transformations @@ -176,7 +174,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPOST(Main main, AppIden } String fullUrl = baseURL + path; - HttpRequestForOry.Response response = HttpRequestForOry.doJsonPost(fullUrl, headers, jsonInput); + HttpRequestForOAuthProvider.Response response = HttpRequestForOAuthProvider.doJsonPost(fullUrl, headers, jsonInput); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); @@ -191,7 +189,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPOST(Main main, AppIden return response; } - public static HttpRequestForOry.Response doOAuthProxyJsonPUT(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 { + public static HttpRequestForOAuthProvider.Response doOAuthProxyJsonPUT(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); @@ -201,9 +199,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPUT(Main main, AppIdent } if (clientIdToCheck != null) { - if (!oauthStorage.doesOAuthClientIdExist(appIdentifier, clientIdToCheck)) { - throw new OAuthClientNotFoundException(); - } + oauthStorage.getOAuthClientById(appIdentifier, clientIdToCheck); // may throw OAuthClientNotFoundException } // Request transformations @@ -218,7 +214,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPUT(Main main, AppIdent } String fullUrl = baseURL + path; - HttpRequestForOry.Response response = HttpRequestForOry.doJsonPut(fullUrl, queryParams, headers, jsonInput); + HttpRequestForOAuthProvider.Response response = HttpRequestForOAuthProvider.doJsonPut(fullUrl, queryParams, headers, jsonInput); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); @@ -233,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, Map queryParams, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequestForOAuthProvider.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); @@ -242,9 +238,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppId } if (clientIdToCheck != null) { - if (!oauthStorage.doesOAuthClientIdExist(appIdentifier, clientIdToCheck)) { - throw new OAuthClientNotFoundException(); - } + oauthStorage.getOAuthClientById(appIdentifier, clientIdToCheck); // may throw OAuthClientNotFoundException } // Request transformations @@ -259,7 +253,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppId } String fullUrl = baseURL + path; - HttpRequestForOry.Response response = HttpRequestForOry.doJsonDelete(fullUrl, queryParams, headers, jsonInput); + HttpRequestForOAuthProvider.Response response = HttpRequestForOAuthProvider.doJsonDelete(fullUrl, headers, queryParams, jsonInput); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); @@ -274,10 +268,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppId return response; } - private static void checkNonSuccessResponse(HttpRequestForOry.Response response) throws OAuthAPIException, OAuthClientNotFoundException { - if (response.statusCode == 404) { - throw new OAuthClientNotFoundException(); - } + private static void checkNonSuccessResponse(HttpRequestForOAuthProvider.Response response) throws OAuthAPIException, OAuthClientNotFoundException { if (response.statusCode >= 400) { String error = response.jsonResponse.getAsJsonObject().get("error").getAsString(); String errorDescription = null; @@ -334,8 +325,6 @@ public static String transformTokensInAuthRedirect(Main main, AppIdentifier appI 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; - System.out.println("transformTokens: " + jsonBody.toString()); - if (jsonBody.has("refresh_token")) { String refreshToken = jsonBody.get("refresh_token").getAsString(); refreshToken = refreshToken.replace("ory_rt_", "st_rt_"); @@ -368,20 +357,54 @@ public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, return jsonBody; } - public static void addOrUpdateClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId, boolean isClientCredentialsOnly) - throws StorageQueryException, TenantOrAppNotFoundException { + 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); - oauthStorage.addOrUpdateOauthClient(appIdentifier, clientId, isClientCredentialsOnly); + clientSecret = encryptClientSecret(main, appIdentifier.getAsPublicTenantIdentifier(), clientSecret); + oauthStorage.addOrUpdateOauthClient(appIdentifier, clientId, clientSecret, isClientCredentialsOnly, enableRefreshTokenRotation); + } + + + 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(tenant, main).getOAuthClientSecretEncryptionKey(); + clientSecret = Utils.encrypt(clientSecret, key); + return clientSecret; } - public static void removeClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException { + 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(tenant, main).getOAuthClientSecretEncryptionKey(); + clientSecret = Utils.decrypt(clientSecret, key); + return clientSecret; + } + + public static boolean removeClient(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - oauthStorage.deleteOAuthClient(appIdentifier, clientId); + return oauthStorage.deleteOAuthClient(appIdentifier, clientId); } - public static List listClientIds(Main main, AppIdentifier appIdentifier, Storage storage) throws StorageQueryException { + 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); - return oauthStorage.listOAuthClients(appIdentifier); + List finalResult = new ArrayList<>(); + List clients = oauthStorage.getOAuthClients(appIdentifier, clientIds); + for (OAuthClient client : clients) { + finalResult.add(new OAuthClient(client.clientId, decryptClientSecret(main, appIdentifier.getAsPublicTenantIdentifier(), client.clientSecret), client.isClientCredentialsOnly, client.enableRefreshTokenRotation)); + } + return finalResult; } private static Map convertCamelToSnakeCase(Map queryParams) { @@ -394,7 +417,7 @@ private static Map convertCamelToSnakeCase(Map q return result; } - public static JsonObject convertCamelToSnakeCase(JsonObject queryParams) { + private static JsonObject convertCamelToSnakeCase(JsonObject queryParams) { JsonObject result = new JsonObject(); for (Map.Entry entry : queryParams.entrySet()) { result.add(Utils.camelCaseToSnakeCase(entry.getKey()), entry.getValue()); @@ -426,12 +449,11 @@ private static JsonElement convertSnakeCaseToCamelCaseRecursively(JsonElement js return result; } return jsonResponse; - } 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); if (!payload.get("active").getAsBoolean()) { @@ -447,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, @@ -524,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) { @@ -553,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 { @@ -623,4 +633,36 @@ public static void deleteLogoutChallenge(Main main, AppIdentifier appIdentifier, OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); 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, TenantOrAppNotFoundException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + OAuthClient client = oauthStorage.getOAuthClientById(appIdentifier, clientId); + if (client.clientSecret != null) { + client = new OAuthClient(client.clientId, decryptClientSecret(main, appIdentifier.getAsPublicTenantIdentifier(), client.clientSecret), client.isClientCredentialsOnly, client.enableRefreshTokenRotation); + } + return client; + } + + public static String getInternalRefreshToken(Main main, AppIdentifier appIdentifier, Storage storage, + String externalRefreshToken) throws StorageQueryException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + String internalRefreshToken = oauthStorage.getRefreshTokenMapping(appIdentifier, externalRefreshToken); + if (internalRefreshToken == null) { + return externalRefreshToken; + } + return internalRefreshToken; + } + + 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.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/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index 9f578d64c..a4e5e4c54 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -17,17 +17,25 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.HttpRequestForOAuthProvider; import io.supertokens.oauth.OAuth; import io.supertokens.oauth.Transformations; import io.supertokens.oauth.exceptions.OAuthAPIException; @@ -38,6 +46,7 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.oauth.OAuthClient; import io.supertokens.pluginInterface.oauth.exception.OAuthClientNotFoundException; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; @@ -60,11 +69,13 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO String clientId = InputParser.getQueryParamOrThrowError(req, "clientId", false); try { - HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyGET( main, req, resp, - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), - clientId, // clientIdToCheck + appIdentifier, + storage, + null, // clientIdToCheck "/admin/clients/" + clientId, // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion @@ -72,13 +83,23 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO new HashMap<>() ); if (response != null) { + OAuthClient client = OAuth.getOAuthClientById(main, appIdentifier, storage, clientId); Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); + if (client.clientSecret != null) { + response.jsonResponse.getAsJsonObject().addProperty("clientSecret", client.clientSecret); + } + response.jsonResponse.getAsJsonObject().addProperty("enableRefreshTokenRotation", client.enableRefreshTokenRotation); response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } - } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + } catch (OAuthClientNotFoundException e) { + OAuthProxyHelper.handleOAuthClientNotFoundException(resp); + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | InvalidKeyException | + NoSuchAlgorithmException | InvalidKeySpecException | InvalidAlgorithmParameterException | + NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException | + StorageQueryException | InvalidConfigException e) { throw new ServletException(e); - } + } } @Override @@ -89,7 +110,16 @@ 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()); + + if (!input.has("clientId")) { + input.addProperty("clientId", "stcl_" + UUID.randomUUID()); + } + + boolean enableRefreshTokenRotation = false; + if (input.has("enableRefreshTokenRotation")) { + enableRefreshTokenRotation = InputParser.parseBooleanOrThrowError(input, "enableRefreshTokenRotation", false); + input.remove("enableRefreshTokenRotation"); + } boolean isClientCredentialsOnly = input.has("grantTypes") && input.get("grantTypes").isJsonArray() && @@ -102,7 +132,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I input.addProperty("owner", appIdentifier.getAppId()); - HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPOST( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyJsonPOST( main, req, resp, appIdentifier, storage, @@ -115,14 +145,19 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I ); if (response != null) { String clientId = response.jsonResponse.getAsJsonObject().get("clientId").getAsString(); + String clientSecret = null; + if (response.jsonResponse.getAsJsonObject().has("clientSecret")) { + clientSecret = response.jsonResponse.getAsJsonObject().get("clientSecret").getAsString(); + } + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); try { - OAuth.addOrUpdateClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId, isClientCredentialsOnly); - } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + OAuth.addOrUpdateClient(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId, clientSecret, isClientCredentialsOnly, enableRefreshTokenRotation); + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | InvalidConfigException e) { throw new ServletException(e); } + response.jsonResponse.getAsJsonObject().addProperty("enableRefreshTokenRotation", enableRefreshTokenRotation); - Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } @@ -135,16 +170,12 @@ 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 { Map queryParams = new HashMap<>(); queryParams.put("clientId", clientId); - HttpRequestForOry.Response response = OAuth.doOAuthProxyGET( + HttpRequestForOAuthProvider.Response response = OAuth.doOAuthProxyGET( main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -166,10 +197,32 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO } try { - HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + OAuthClient client = OAuth.getOAuthClientById(main, appIdentifier, storage, clientId); + + if (input.has("grantTypes")) { + 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"); + client = new OAuthClient(clientId, client.clientSecret, isClientCredentialsOnly, client.enableRefreshTokenRotation); + } + + if (input.has("clientSecret")) { + String clientSecret = InputParser.parseStringOrThrowError(input, "clientSecret", false); + client = new OAuthClient(clientId, clientSecret, client.isClientCredentialsOnly, client.enableRefreshTokenRotation); + } + + if (input.has("enableRefreshTokenRotation")) { + boolean enableRefreshTokenRotation = InputParser.parseBooleanOrThrowError(input, "enableRefreshTokenRotation", false); + client = new OAuthClient(clientId, client.clientSecret, client.isClientCredentialsOnly, enableRefreshTokenRotation); + } + + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyJsonPUT( main, req, resp, - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), + appIdentifier, + storage, clientId, // clientIdToCheck "/admin/clients/" + clientId, true, // proxyToAdmin @@ -181,17 +234,25 @@ 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) { + OAuth.addOrUpdateClient(main, appIdentifier, storage, clientId, client.clientSecret, client.isClientCredentialsOnly, client.enableRefreshTokenRotation); + } catch (StorageQueryException | TenantOrAppNotFoundException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | InvalidConfigException e) { throw new ServletException(e); } Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); + + if (!response.jsonResponse.getAsJsonObject().has("clientSecret")) { + response.jsonResponse.getAsJsonObject().addProperty("clientSecret", client.clientSecret); + } + response.jsonResponse.getAsJsonObject().addProperty("enableRefreshTokenRotation", client.enableRefreshTokenRotation); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } - } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + } catch (IOException | StorageQueryException | TenantOrAppNotFoundException | BadPermissionException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | InvalidConfigException e) { throw new ServletException(e); + } catch (OAuthClientNotFoundException e) { + OAuthProxyHelper.handleOAuthClientNotFoundException(resp); } } } 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 1579dfdd2..2b1fce705 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java @@ -8,7 +8,7 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.HttpRequestForOAuthProvider; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.InputParser; @@ -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 @@ -63,7 +66,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO input.add("session", session); try { - HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyJsonPUT( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java index 792f01539..7ff74153f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java @@ -6,10 +6,10 @@ import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.oauth.HttpRequestForOAuthProvider; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.webserver.WebserverAPI; import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.HttpRequestForOry; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.InputParser; import jakarta.servlet.ServletException; @@ -32,7 +32,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject input = InputParser.parseJsonObjectOrThrowError(req); try { - HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyJsonPUT( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java index 052c47465..786f0fbe8 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java @@ -1,13 +1,11 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.util.HashMap; 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.oauth.exceptions.OAuthAPIException; import io.supertokens.pluginInterface.RECIPE_ID; 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 7ff44dcfc..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,15 +19,24 @@ 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.HttpRequestForOry; +import io.supertokens.oauth.HttpRequestForOAuthProvider; 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.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; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -78,7 +87,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyGET( main, req, resp, appIdentifier, storage, @@ -98,6 +107,39 @@ 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); + + 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 { + 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); + } + } + } + List responseCookies = response.headers.get("Set-Cookie"); JsonObject finalResponse = new JsonObject(); @@ -116,8 +158,27 @@ 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); } } + + private void updateLastActive(AppIdentifier appIdentifier, String sessionHandle) { + try { + TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifier.getConnectionUriDomain(), + appIdentifier.getAppId(), Session.getTenantIdFromSessionHandle(sessionHandle)); + Storage storage = StorageLayer.getStorage(tenantIdentifier, main); + SessionInfo sessionInfo = Session.getSession(tenantIdentifier, storage, sessionHandle); + + UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping( + appIdentifier, storage, sessionInfo.userId, UserIdType.ANY); + if (userIdMapping != null) { + ActiveUsers.updateLastActive(appIdentifier, main, userIdMapping.superTokensUserId); + } else { + ActiveUsers.updateLastActive(appIdentifier, main, sessionInfo.userId); + } + } catch (Exception e) { + // ignore + } + } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java index 00eff45d3..b728656f9 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java @@ -1,11 +1,14 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -13,18 +16,24 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.HttpRequestForOAuthProvider; 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.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.oauth.OAuthClient; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + public class OAuthClientListAPI extends WebserverAPI { public OAuthClientListAPI(Main main) { @@ -44,7 +53,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO Map queryParams = OAuthProxyHelper.defaultGetQueryParamsFromRequest(req); queryParams.put("owner", appIdentifier.getAppId()); - HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyGET( main, req, resp, appIdentifier, storage, @@ -61,19 +70,30 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO finalResponse.addProperty("status", "OK"); // Filter out the clients for app - List clientIds; + Map clientsMap = new HashMap<>(); try { - clientIds = OAuth.listClientIds(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req)); - } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + List clientIds = new ArrayList<>(); + for (JsonElement clientElem : response.jsonResponse.getAsJsonArray()) { + clientIds.add(clientElem.getAsJsonObject().get("clientId").getAsString()); + } + + List clients = OAuth.getClients(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientIds); + for (OAuthClient client : clients) { + clientsMap.put(client.clientId, client); + } + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException | + InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | + NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | + BadPaddingException | InvalidConfigException e) { throw new ServletException(e); } - Set clientIdsSet = new HashSet<>(clientIds); - JsonArray clients = new JsonArray(); for (JsonElement clientElem : response.jsonResponse.getAsJsonArray()) { - if (clientIdsSet.contains(clientElem.getAsJsonObject().get("clientId").getAsString())) { + if (clientsMap.containsKey(clientElem.getAsJsonObject().get("clientId").getAsString())) { + clientElem.getAsJsonObject().addProperty("clientSecret", clientsMap.get(clientElem.getAsJsonObject().get("clientId").getAsString()).clientSecret); + clientElem.getAsJsonObject().addProperty("enableRefreshTokenRotation", clientsMap.get(clientElem.getAsJsonObject().get("clientId").getAsString()).enableRefreshTokenRotation); clients.add(clientElem); } } 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 37f66ae6e..326e9041f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java @@ -5,7 +5,7 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.HttpRequestForOAuthProvider; import io.supertokens.oauth.Transformations; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; @@ -28,7 +28,7 @@ public String getPath() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { try { - HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyGET( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), 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 cc3c06b2c..a1d07e849 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java @@ -1,14 +1,29 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; import java.util.HashMap; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.HttpRequestForOAuthProvider; +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.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.oauth.OAuthClient; +import io.supertokens.pluginInterface.oauth.exception.OAuthClientNotFoundException; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -28,27 +43,44 @@ public String getPath() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { try { - HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( - main, req, resp, - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), - null, // clientIdToCheck - "/admin/oauth2/auth/requests/login", // proxyPath - true, // proxyToAdmin - true, // camelToSnakeCaseConversion - OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - new HashMap<>() // headers + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyGET( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/oauth2/auth/requests/login", // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + new HashMap<>() // headers ); if (response != null) { - Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject()); + Transformations.applyClientPropsWhiteList( + response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject()); + + String clientId = response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject() + .get("clientId").getAsString(); + OAuthClient client = OAuth.getOAuthClientById(main, appIdentifier, storage, clientId); + + response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject() + .addProperty("enableRefreshTokenRotation", client.enableRefreshTokenRotation); + response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject().addProperty("clientSecret", + client.clientSecret); response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } - } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException + | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException + | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException + | StorageQueryException | InvalidConfigException e) { throw new ServletException(e); + } catch (OAuthClientNotFoundException e) { + OAuthProxyHelper.handleOAuthClientNotFoundException(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 index fba039346..d794a6ba9 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java @@ -9,7 +9,7 @@ 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.HttpRequestForOAuthProvider; import io.supertokens.oauth.OAuth; import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.pluginInterface.RECIPE_ID; @@ -77,7 +77,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO // Check if the post logout redirection URI is valid for the clientId if (postLogoutRedirectionUri != null) { - HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyGET( main, req, resp, appIdentifier, storage, clientId, // clientIdToCheck 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 592eeebd4..6a3476f94 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java @@ -11,7 +11,7 @@ import io.supertokens.Main; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; -import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.HttpRequestForOAuthProvider; import io.supertokens.oauth.OAuth; import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.pluginInterface.Storage; @@ -28,9 +28,9 @@ public class OAuthProxyHelper { @Serial private static final long serialVersionUID = -8734479943734920904L; - public static HttpRequestForOry.Response proxyGET(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, - String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, - Map queryParams, Map headers) throws IOException, ServletException { + public static HttpRequestForOAuthProvider.Response proxyGET(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + Map queryParams, Map headers) throws IOException, ServletException { try { return OAuth.doOAuthProxyGET(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, headers); @@ -44,9 +44,9 @@ public static HttpRequestForOry.Response proxyGET(Main main, HttpServletRequest return null; } - public static HttpRequestForOry.Response proxyFormPOST(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, - String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, - Map formFields, Map headers) throws IOException, ServletException { + public static HttpRequestForOAuthProvider.Response proxyFormPOST(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + Map formFields, Map headers) throws IOException, ServletException { try { return OAuth.doOAuthProxyFormPOST(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, formFields, headers); } catch (OAuthClientNotFoundException e) { @@ -59,9 +59,9 @@ public static HttpRequestForOry.Response proxyFormPOST(Main main, HttpServletReq return null; } - public static HttpRequestForOry.Response proxyJsonPOST(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 { + public static HttpRequestForOAuthProvider.Response proxyJsonPOST(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 { try { return OAuth.doOAuthProxyJsonPOST(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); } catch (OAuthClientNotFoundException e) { @@ -74,9 +74,9 @@ public static HttpRequestForOry.Response proxyJsonPOST(Main main, HttpServletReq return null; } - public static HttpRequestForOry.Response proxyJsonPUT(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, - String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, - Map queryParams, JsonObject jsonInput, Map headers) throws IOException, ServletException { + public static HttpRequestForOAuthProvider.Response proxyJsonPUT(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + Map queryParams, JsonObject jsonInput, Map headers) throws IOException, ServletException { try { return OAuth.doOAuthProxyJsonPUT(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); @@ -90,9 +90,9 @@ public static HttpRequestForOry.Response proxyJsonPUT(Main main, HttpServletRequ return null; } - public static HttpRequestForOry.Response proxyJsonDELETE(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, - String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, - Map queryParams, JsonObject jsonInput, Map headers) throws IOException, ServletException { + public static HttpRequestForOAuthProvider.Response proxyJsonDELETE(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + Map queryParams, JsonObject jsonInput, Map headers) throws IOException, ServletException { try { return OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); } catch (OAuthClientNotFoundException e) { diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java index ef4fed870..173af8812 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java @@ -7,7 +7,7 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.HttpRequestForOAuthProvider; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.InputParser; @@ -32,7 +32,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject input = InputParser.parseJsonObjectOrThrowError(req); try { - HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyJsonPUT( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java index 6462d358c..ec93e41c0 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java @@ -8,7 +8,7 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.HttpRequestForOAuthProvider; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.InputParser; @@ -33,7 +33,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject input = InputParser.parseJsonObjectOrThrowError(req); try { - HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyJsonPUT( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java index 42bf9dd84..2a9118cfc 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java @@ -1,13 +1,11 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.util.HashMap; 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.exceptions.StorageQueryException; 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 843fc0245..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,13 +17,18 @@ 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; 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.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; @@ -31,19 +36,33 @@ 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.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.JWTException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.useridmapping.UserIdType; +import io.supertokens.utils.Utils; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; 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 { @@ -83,18 +102,33 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I formFields.put(entry.getKey(), entry.getValue().getAsString()); } + String clientId; + + if (authorizationHeader != null) { + String[] parsedHeader = Utils.convertFromBase64(authorizationHeader.replaceFirst("^Basic ", "").trim()).split(":"); + clientId = parsedHeader[0]; + } else { + clientId = formFields.get("client_id"); + } + try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + OAuthClient oauthClient = OAuth.getOAuthClientById(main, appIdentifier, storage, clientId); + + String inputRefreshToken = null; // check if the refresh token is valid if (grantType.equals("refresh_token")) { String refreshToken = InputParser.parseStringOrThrowError(bodyFromSDK, "refresh_token", false); + inputRefreshToken = refreshToken; + + String internalRefreshToken = OAuth.getInternalRefreshToken(main, appIdentifier, storage, refreshToken); Map formFieldsForTokenIntrospect = new HashMap<>(); - formFieldsForTokenIntrospect.put("token", refreshToken); + formFieldsForTokenIntrospect.put("token", internalRefreshToken); - HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyFormPOST( main, req, resp, appIdentifier, storage, @@ -113,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); @@ -126,13 +160,15 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I )); return; } + + formFields.put("refresh_token", internalRefreshToken); } - HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyFormPOST( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), - formFields.get("client_id"), // clientIdToCheck + clientId, // clientIdToCheck "/oauth2/token", // proxyPath false, // proxyToAdmin false, // camelToSnakeCaseConversion @@ -147,20 +183,111 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I if (grantType.equals("client_credentials")) { try { OAuth.addM2MToken(main, appIdentifier, storage, response.jsonResponse.getAsJsonObject().get("access_token").getAsString()); - } catch (Exception e) { - // ignore + } catch (TryRefreshTokenException e) { + throw new IllegalStateException("should never happen"); } } - } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { + 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<>(); + formFieldsForTokenIntrospect.put("token", newRefreshToken); + + HttpRequestForOAuthProvider.Response introspectResponse = OAuthProxyHelper.proxyFormPOST( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck + "/admin/oauth2/introspect", // pathProxy + true, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFieldsForTokenIntrospect, + new HashMap<>() // headers + ); + + if (introspectResponse != null) { + JsonObject refreshTokenPayload = introspectResponse.jsonResponse.getAsJsonObject(); + refreshTokenExp = refreshTokenPayload.get("exp").getAsLong(); + } else { + throw new IllegalStateException("Should never come here"); + } + } + + if (inputRefreshToken == null) { + // 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.createOrUpdateOauthSession(main, appIdentifier, storage, clientId, gid, inputRefreshToken, newRefreshToken, sessionHandle, List.of(jti), refreshTokenExp); + response.jsonResponse.getAsJsonObject().remove("refresh_token"); + } else { + OAuth.createOrUpdateOauthSession(main, appIdentifier, storage, clientId, gid, newRefreshToken, null, sessionHandle, List.of(jti), refreshTokenExp); + } + } + } else { + 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 | OAuthClientNotFoundException e) { throw new ServletException(e); } response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } - } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + } catch (IOException | StorageQueryException | TenantOrAppNotFoundException | BadPermissionException | + InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | + InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | + InvalidConfigException e) { throw new ServletException(e); + } catch (OAuthClientNotFoundException e) { + OAuthProxyHelper.handleOAuthClientNotFoundException(resp); + } + } + + private void updateLastActive(AppIdentifier appIdentifier, String sessionHandle) { + try { + TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifier.getConnectionUriDomain(), + appIdentifier.getAppId(), Session.getTenantIdFromSessionHandle(sessionHandle)); + Storage storage = StorageLayer.getStorage(tenantIdentifier, main); + SessionInfo sessionInfo = Session.getSession(tenantIdentifier, storage, sessionHandle); + + UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping( + appIdentifier, storage, sessionInfo.userId, UserIdType.ANY); + if (userIdMapping != null) { + ActiveUsers.updateLastActive(appIdentifier, main, userIdMapping.superTokensUserId); + } else { + ActiveUsers.updateLastActive(appIdentifier, main, sessionInfo.userId); + } + } catch (Exception e) { + // ignore } } } 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 1bf281f8e..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,12 +16,13 @@ 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; import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.HttpRequestForOAuthProvider; import io.supertokens.oauth.OAuth; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; @@ -65,7 +66,11 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + + token = OAuth.getInternalRefreshToken(main, appIdentifier, storage, token); + formFields.put("token", token); + + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyFormPOST( main, req, resp, appIdentifier, storage, @@ -79,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); @@ -90,7 +98,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I finalResponse.addProperty("status", "OK"); super.sendJsonResponse(200, finalResponse, resp); } - } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + } catch (IOException | StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); } } else { @@ -98,6 +106,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); JsonObject response = OAuth.introspectAccessToken(main, appIdentifier, storage, token); + response.addProperty("status", "OK"); super.sendJsonResponse(200, response, resp); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { 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 6844eff81..532821d37 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java @@ -24,10 +24,12 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.HttpRequestForOAuthProvider; 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; @@ -52,11 +54,28 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String clientId = InputParser.parseStringOrThrowError(input, "clientId", false); try { - HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonDELETE( + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + try { + boolean didExist = OAuth.removeClient(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); + if (!didExist) { + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + response.addProperty("didExist", false); + super.sendJsonResponse(200, response, resp); + return; + } + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + + + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyJsonDELETE( main, req, resp, - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), - clientId, // clientIdToCheck + appIdentifier, + storage, + null, // clientIdToCheck "/admin/clients/" + clientId, // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion @@ -66,14 +85,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I ); if (response != null) { - try { - OAuth.removeClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); - } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { - throw new ServletException(e); - } - JsonObject finalResponse = new JsonObject(); finalResponse.addProperty("status", "OK"); + finalResponse.addProperty("didExist", true); super.sendJsonResponse(200, finalResponse, resp); } 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 b71ddd6d6..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,17 +1,11 @@ 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.HttpRequestForOAuthProvider; import io.supertokens.oauth.OAuth; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; @@ -20,12 +14,18 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.utils.Utils; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; 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()); @@ -46,14 +46,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); if (token.startsWith("st_rt_")) { + token = OAuth.getInternalRefreshToken(main, appIdentifier, storage, token); + String gid = null; long exp = -1; { // introspect token to get gid Map formFields = new HashMap<>(); formFields.put("token", token); - - HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyFormPOST( main, req, resp, appIdentifier, storage, @@ -67,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(); @@ -82,11 +88,21 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } // revoking refresh token - String clientId = InputParser.parseStringOrThrowError(input, "client_id", false); - String clientSecret = InputParser.parseStringOrThrowError(input, "client_secret", true); + + String clientId, clientSecret; String authorizationHeader = InputParser.parseStringOrThrowError(input, "authorizationHeader", true); + if (authorizationHeader != null) { + String[] parsedHeader = Utils.convertFromBase64(authorizationHeader.replaceFirst("^Basic ", "").trim()).split(":"); + clientId = parsedHeader[0]; + clientSecret = parsedHeader[1]; + } else { + clientId = InputParser.parseStringOrThrowError(input, "client_id", false); + clientSecret = InputParser.parseStringOrThrowError(input, "client_secret", true); + } + + Map headers = new HashMap<>(); if (authorizationHeader != null) { headers.put("Authorization", authorizationHeader); @@ -99,7 +115,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I formFields.put("client_secret", clientSecret); } - HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyFormPOST( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -115,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/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java index 6aef1c3ef..754912f19 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java @@ -8,7 +8,7 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.HttpRequestForOAuthProvider; import io.supertokens.oauth.OAuth; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; @@ -46,7 +46,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I Map queryParams = new HashMap<>(); queryParams.put("client_id", clientId); - HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonDELETE( + HttpRequestForOAuthProvider.Response response = OAuthProxyHelper.proxyJsonDELETE( main, req, resp, appIdentifier, storage, @@ -60,8 +60,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I ); if (response != null) { - response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); - super.sendJsonResponse(200, response.jsonResponse, resp); + if (response.statusCode == 204) { + JsonObject finalResponse = new JsonObject(); + finalResponse.addProperty("status", "OK"); + super.sendJsonResponse(200, finalResponse, resp); + } else { + throw new IllegalStateException("should never come here"); + } } } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException e) { throw new ServletException(e); diff --git a/src/test/java/io/supertokens/test/Utils.java b/src/test/java/io/supertokens/test/Utils.java index 550e80b2e..0657dcad4 100644 --- a/src/test/java/io/supertokens/test/Utils.java +++ b/src/test/java/io/supertokens/test/Utils.java @@ -35,10 +35,12 @@ import org.mockito.Mockito; import java.io.*; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import static org.junit.Assert.*; @@ -271,4 +273,16 @@ public static void assertArrayEqualsIgnoreOrder(T[] array1, T[] array2) { array1.length == array2.length && Arrays.asList(array1).containsAll(Arrays.asList(array2)) && Arrays.asList(array2).containsAll(Arrays.asList(array1))); } + + public static java.util.Map splitQueryString(String query) throws UnsupportedEncodingException { + java.util.Map queryParams = new HashMap<>(); + String[] pairs = query.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair; + String value = idx > 0 && pair.length() > idx + 1 ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : null; + queryParams.put(key, value); + } + return queryParams; + } } diff --git a/src/test/java/io/supertokens/test/multitenant/TestAppData.java b/src/test/java/io/supertokens/test/multitenant/TestAppData.java index 71d895b5d..e4d38b37c 100644 --- a/src/test/java/io/supertokens/test/multitenant/TestAppData.java +++ b/src/test/java/io/supertokens/test/multitenant/TestAppData.java @@ -177,7 +177,7 @@ null, null, new JsonObject() UserIdMapping.createUserIdMapping(process.getProcess(), app.toAppIdentifier(), appStorage, plUser.user.getSupertokensUserId(), "externalid", null, false); - OAuth.addOrUpdateClientId(process.getProcess(), app.toAppIdentifier(), appStorage, "test", false); + OAuth.addOrUpdateClient(process.getProcess(), app.toAppIdentifier(), appStorage, "test", "secret123", false, false); OAuth.createLogoutRequestAndReturnRedirectUri(process.getProcess(), app.toAppIdentifier(), appStorage, "test", "http://localhost", "sessionHandle", "state"); ((OAuthStorage) appStorage).addOAuthM2MTokenForStats(app.toAppIdentifier(), "test", 1000, 2000); OAuth.revokeSessionHandle(process.getProcess(), app.toAppIdentifier(), appStorage, "sessionHandle"); diff --git a/src/test/java/io/supertokens/test/oauth/OAuthStorageTest.java b/src/test/java/io/supertokens/test/oauth/OAuthStorageTest.java index 6d778a037..743362b46 100644 --- a/src/test/java/io/supertokens/test/oauth/OAuthStorageTest.java +++ b/src/test/java/io/supertokens/test/oauth/OAuthStorageTest.java @@ -20,8 +20,8 @@ import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; 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; @@ -34,6 +34,7 @@ import org.junit.Test; import org.junit.rules.TestRule; +import java.util.ArrayList; import java.util.List; import static org.junit.Assert.*; @@ -66,26 +67,45 @@ public void testClientCRUD() throws Exception { OAuthStorage storage = (OAuthStorage) StorageLayer.getStorage(process.getProcess()); AppIdentifier appIdentifier = new AppIdentifier(null, null); - assertEquals(0, storage.listOAuthClients(appIdentifier).size()); + assertEquals(0, storage.getOAuthClients(appIdentifier, new ArrayList<>()).size()); // TODO fix me - storage.addOrUpdateOauthClient(appIdentifier, "clientid1", false); - storage.addOrUpdateOauthClient(appIdentifier, "clientid2", true); + storage.addOrUpdateOauthClient(appIdentifier, "clientid1", "secret123", false, false); + storage.addOrUpdateOauthClient(appIdentifier, "clientid2", "secret123", true, false); - assertTrue(storage.doesOAuthClientIdExist(appIdentifier, "clientid1")); - assertFalse(storage.doesOAuthClientIdExist(appIdentifier, "clientid3")); + OAuthClient client = storage.getOAuthClientById(appIdentifier, "clientid1"); + assertNotNull(client); + assertEquals("secret123", client.clientSecret); + assertFalse(client.isClientCredentialsOnly); + assertFalse(client.enableRefreshTokenRotation); + + try { + storage.getOAuthClientById(appIdentifier, "clientid3"); + fail(); + } catch (OAuthClientNotFoundException e) { + // ignore + } assertEquals(2, storage.countTotalNumberOfOAuthClients(appIdentifier)); assertEquals(1, storage.countTotalNumberOfClientCredentialsOnlyOAuthClients(appIdentifier)); - assertEquals(List.of("clientid1", "clientid2"), storage.listOAuthClients(appIdentifier)); + List clients = storage.getOAuthClients(appIdentifier, List.of("clientid1", "clientid2")); + assertEquals(2, clients.size()); storage.deleteOAuthClient(appIdentifier, "clientid1"); - assertEquals(List.of("clientid2"), storage.listOAuthClients(appIdentifier)); + clients = storage.getOAuthClients(appIdentifier, List.of("clientid1", "clientid2")); + assertEquals(1, clients.size()); assertEquals(1, storage.countTotalNumberOfClientCredentialsOnlyOAuthClients(appIdentifier)); - storage.addOrUpdateOauthClient(appIdentifier, "clientid2", false); + storage.addOrUpdateOauthClient(appIdentifier, "clientid2", "secret123", false, false); assertEquals(0, storage.countTotalNumberOfClientCredentialsOnlyOAuthClients(appIdentifier)); + // Test all field updates + storage.addOrUpdateOauthClient(appIdentifier, "clientid2", "newsecret", true, true); + client = storage.getOAuthClientById(appIdentifier, "clientid2"); + assertEquals("newsecret", client.clientSecret); + assertTrue(client.isClientCredentialsOnly); + assertTrue(client.enableRefreshTokenRotation); + process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } @@ -105,7 +125,7 @@ public void testLogoutChallenge() throws Exception { AppIdentifier appIdentifier = new AppIdentifier(null, null); - storage.addOrUpdateOauthClient(appIdentifier, "clientid", false); + storage.addOrUpdateOauthClient(appIdentifier, "clientid", "secret123", false, false); // Test nulls storage.addOAuthLogoutChallenge(appIdentifier, "challengeid", "clientid", null, null, null, System.currentTimeMillis()); @@ -156,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.cleanUpExpiredAndRevokedOAuthTokensList(); - - 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)); @@ -236,7 +224,7 @@ public void testM2MTokenAndStats() throws Exception { long now = System.currentTimeMillis() / 1000; - storage.addOrUpdateOauthClient(appIdentifier, "clientid", true); + storage.addOrUpdateOauthClient(appIdentifier, "clientid", "secret123", true, false); storage.addOAuthM2MTokenForStats(appIdentifier, "clientid", now - 3600 - 2, now + 2); storage.addOAuthM2MTokenForStats(appIdentifier, "clientid", now - 3600 * 24 - 2, now + 2); @@ -266,7 +254,7 @@ public void testConstraints() throws Exception { OAuthStorage storage = (OAuthStorage) StorageLayer.getStorage(process.getProcess()); AppIdentifier appIdentifier = new AppIdentifier(null, null); - storage.addOrUpdateOauthClient(appIdentifier, "clientid", false); + storage.addOrUpdateOauthClient(appIdentifier, "clientid", "secret123", false, false); // PK { @@ -283,25 +271,20 @@ 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 AppIdentifier appIdentifier2 = new AppIdentifier(null,"a1"); try { - storage.addOrUpdateOauthClient(appIdentifier2, "clientid", false); - fail(); - } catch (TenantOrAppNotFoundException e) { - // expected - } - try { - storage.revokeOAuthTokensBasedOnTargetFields(appIdentifier2, OAuthRevokeTargetType.GID, "abcd", 0); + storage.addOrUpdateOauthClient(appIdentifier2, "clientid", "secret123", false, false); fail(); } catch (TenantOrAppNotFoundException e) { // expected } + assertFalse(storage.revokeOAuthTokenByGID(appIdentifier2, "abcd")); + // Client FK try { storage.addOAuthLogoutChallenge(appIdentifier2, "challenge1", "clientid", null, null, null, 0); @@ -328,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/OAuthAPIHelper.java b/src/test/java/io/supertokens/test/oauth/api/OAuthAPIHelper.java new file mode 100644 index 000000000..fa490d564 --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/OAuthAPIHelper.java @@ -0,0 +1,147 @@ +/* + * 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 java.util.HashMap; +import java.util.Map; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.oauth.HttpRequestForOAuthProvider; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.utils.SemVer; + +public class OAuthAPIHelper { + public static void resetOAuthProvider() { + try { + HttpRequestForOAuthProvider.Response clientsResponse = HttpRequestForOAuthProvider + .doGet("http://localhost:4445/admin/clients", new HashMap<>(), new HashMap<>()); + + for (JsonElement client : clientsResponse.jsonResponse.getAsJsonArray()) { + HttpRequestForOAuthProvider.doJsonDelete( + "http://localhost:4445/admin/clients/" + + client.getAsJsonObject().get("client_id").getAsString(), + new HashMap<>(), new HashMap<>(), new JsonObject()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static JsonObject createClient(Main main, JsonObject createClientBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/clients", createClientBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + public static JsonObject updateClient(Main main, JsonObject updateClientBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(main, "", + "http://localhost:3567/recipe/oauth/clients", updateClientBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + public static JsonObject auth(Main main, JsonObject authBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/auth", authBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + public static JsonObject token(Main main, JsonObject tokenBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/token", tokenBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + public static JsonObject acceptLoginRequest(Main main, Map queryParams, + JsonObject acceptLoginChallengeBody) throws Exception { + String url = "http://localhost:3567/recipe/oauth/auth/requests/login/accept"; + if (queryParams != null && !queryParams.isEmpty()) { + StringBuilder queryString = new StringBuilder("?"); + for (Map.Entry entry : queryParams.entrySet()) { + if (queryString.length() > 1) { + queryString.append("&"); + } + String encodedValue = URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.toString()); + queryString.append(entry.getKey()).append("=").append(encodedValue); + } + url += queryString.toString(); + } + + JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(main, "", + url, acceptLoginChallengeBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + public static JsonObject acceptConsentRequest(Main main, Map queryParams, + JsonObject acceptConsentChallengeBody) throws Exception { + String url = "http://localhost:3567/recipe/oauth/auth/requests/consent/accept"; + if (queryParams != null && !queryParams.isEmpty()) { + StringBuilder queryString = new StringBuilder("?"); + for (Map.Entry entry : queryParams.entrySet()) { + if (queryString.length() > 1) { + queryString.append("&"); + } + String encodedValue = URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.toString()); + queryString.append(entry.getKey()).append("=").append(encodedValue); + } + url += queryString.toString(); + } + + JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(main, "", + url, acceptConsentChallengeBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + public static JsonObject revoke(Main main, JsonObject revokeRequestBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/token/revoke", revokeRequestBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + public static JsonObject introspect(Main main, JsonObject introspectRequestBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/introspect", introspectRequestBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + public static JsonObject revokeClientId(Main main, JsonObject revokeClientIdRequestBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/tokens/revoke", revokeClientIdRequestBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + public static JsonObject revokeSessionHandle(Main main, JsonObject revokeSessionHandleRequestBody) + throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/session/revoke", revokeSessionHandleRequestBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } +} diff --git a/src/test/java/io/supertokens/test/oauth/api/TestAuthCodeFlow.java b/src/test/java/io/supertokens/test/oauth/api/TestAuthCodeFlow.java new file mode 100644 index 000000000..841f69de8 --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/TestAuthCodeFlow.java @@ -0,0 +1,241 @@ +/* + * 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.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.totp.TotpLicenseTest; +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.assertNotNull; + +public class TestAuthCodeFlow { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + OAuthAPIHelper.resetOAuthProvider(); + } + + @Test + public void testAuthCodeGrantFlow() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH}); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + 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(process.getProcess(), clientBody); + + 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(process.getProcess(), 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", "someuserid"); + acceptLoginRequestBody.addProperty("remember", true); + acceptLoginRequestBody.addProperty("rememberFor", 3600); + acceptLoginRequestBody.addProperty("identityProviderSessionId", "session-handle"); + + JsonObject acceptLoginRequestResponse = OAuthAPIHelper.acceptLoginRequest(process.getProcess(), 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(process.getProcess(), 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", "someuserid"); + acceptConsentRequestBody.addProperty("sessionHandle", "session-handle"); + 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(); + JsonObject accessToken = new JsonObject(); + accessToken.addProperty("gid", "gidForTesting"); + session.add("access_token", accessToken); + session.add("id_token", new JsonObject()); + acceptConsentRequestBody.add("session", session); + + queryParams = new HashMap<>(); + queryParams.put("consentChallenge", consentChallenge); + + JsonObject acceptConsentRequestResponse = OAuthAPIHelper.acceptConsentRequest(process.getProcess(), 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(process.getProcess(), 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(process.getProcess(), tokenRequestBody); + + assertNotNull(tokenResponse.get("access_token")); + assertNotNull(tokenResponse.get("id_token")); + assertNotNull(tokenResponse.get("refresh_token")); + assertNotNull(tokenResponse.get("expires_in")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + // Helper method to split query parameters + 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; + } +} diff --git a/src/test/java/io/supertokens/test/oauth/api/TestClientCreate5_2.java b/src/test/java/io/supertokens/test/oauth/api/TestClientCreate5_2.java new file mode 100644 index 000000000..a256fd981 --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/TestClientCreate5_2.java @@ -0,0 +1,305 @@ +/* + * 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.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.test.totp.TotpLicenseTest; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + +public class TestClientCreate5_2 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + OAuthAPIHelper.resetOAuthProvider(); + } + + @Test + public void testInvalidInputs() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH}); + + { + // Duplicate client id + JsonObject createBody = new JsonObject(); + createBody.addProperty("clientId", "test-client-id"); + + JsonObject resp = createClient(process.getProcess(), createBody); + assertEquals("OK", resp.get("status").getAsString()); + + resp = createClient(process.getProcess(), createBody); + assertEquals("OAUTH_ERROR", resp.get("status").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testDefaultClientIdGeneration() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH}); + + JsonObject createBody = new JsonObject(); + + JsonObject resp = createClient(process.getProcess(), createBody); + assertEquals("OK", resp.get("status").getAsString()); + resp.remove("status"); + verifyStructure(resp); + + assertTrue(resp.get("clientId").getAsString().startsWith("stcl_")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testAllFields() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH}); + + String[] FIELDS = new String[]{ + "clientId", + "clientSecret", + "clientName", + "scope", + "redirectUris", + "enableRefreshTokenRotation", + "authorizationCodeGrantAccessTokenLifespan", + "authorizationCodeGrantIdTokenLifespan", + "authorizationCodeGrantRefreshTokenLifespan", + "clientCredentialsGrantAccessTokenLifespan", + "implicitGrantAccessTokenLifespan", + "implicitGrantIdTokenLifespan", + "refreshTokenGrantAccessTokenLifespan", + "refreshTokenGrantIdTokenLifespan", + "refreshTokenGrantRefreshTokenLifespan", + "tokenEndpointAuthMethod", + "clientUri", + "allowedCorsOrigins", + "audience", + "grantTypes", + "responseTypes", + "logoUri", + "policyUri", + "tosUri", + "metadata", + "postLogoutRedirectUris" + }; + + JsonArray redirectUris = new JsonArray(); + redirectUris.add(new JsonPrimitive("http://localhost:3000/auth")); + + JsonArray allowedCorsOrigins = new JsonArray(); + allowedCorsOrigins.add(new JsonPrimitive("http://localhost:3000")); + + JsonArray audience = new JsonArray(); + audience.add(new JsonPrimitive("https://api.example.com")); + + JsonArray grantTypes = new JsonArray(); + grantTypes.add(new JsonPrimitive("authorization_code")); + grantTypes.add(new JsonPrimitive("implicit")); + grantTypes.add(new JsonPrimitive("refresh_token")); + grantTypes.add(new JsonPrimitive("client_credentials")); + + JsonArray responseTypes = new JsonArray(); + responseTypes.add(new JsonPrimitive("code")); + responseTypes.add(new JsonPrimitive("token")); + responseTypes.add(new JsonPrimitive("code token")); + responseTypes.add(new JsonPrimitive("id_token")); + responseTypes.add(new JsonPrimitive("code id_token")); + responseTypes.add(new JsonPrimitive("token id_token")); + responseTypes.add(new JsonPrimitive("code token id_token")); + + JsonArray postRedirectLogoutUris = new JsonArray(); + postRedirectLogoutUris.add(new JsonPrimitive("http://localhost:3000/logout")); + + JsonObject metadata = new JsonObject(); + metadata.addProperty("sub", "test-client-id"); + metadata.addProperty("iss", "http://localhost:3000"); + metadata.addProperty("aud", "https://api.example.com"); + metadata.addProperty("exp", 1714857600); + metadata.addProperty("iat", 1714857600); + metadata.addProperty("jti", "test-jti"); + metadata.addProperty("nonce", "test-nonce"); + + JsonElement[] VALUES = new JsonElement[]{ + new JsonPrimitive("test-client-id"), // clientId + new JsonPrimitive("kEdQVPNLsl_FHOFBO_nWnj7P3."), // clientSecret + new JsonPrimitive("Test Client"), // clientName + new JsonPrimitive("offline_access offline openid"), // scope + redirectUris, // redirectUris + new JsonPrimitive(true), // enableRefreshTokenRotation + new JsonPrimitive("1h0m0s"), // authorizationCodeGrantAccessTokenLifespan + new JsonPrimitive("2h0m0s"), // authorizationCodeGrantIdTokenLifespan + new JsonPrimitive("3h0m0s"), // authorizationCodeGrantRefreshTokenLifespan + new JsonPrimitive("4h0m0s"), // clientCredentialsGrantAccessTokenLifespan + new JsonPrimitive("5h0m0s"), // implicitGrantAccessTokenLifespan + new JsonPrimitive("6h0m0s"), // implicitGrantIdTokenLifespan + new JsonPrimitive("7h0m0s"), // refreshTokenGrantAccessTokenLifespan + new JsonPrimitive("8h0m0s"), // refreshTokenGrantIdTokenLifespan + new JsonPrimitive("9h0m0s"), // refreshTokenGrantRefreshTokenLifespan + new JsonPrimitive("client_secret_post"), // tokenEndpointAuthMethod + new JsonPrimitive("http://localhost:3000"), // clientUri + allowedCorsOrigins, // allowedCorsOrigins + audience, // audience + grantTypes, // grantTypes + responseTypes, // responseTypes + new JsonPrimitive("http://localhost:3000/logo.png"), // logoUri + new JsonPrimitive("http://localhost:3000/policy.html"), // policyUri + new JsonPrimitive("http://localhost:3000/tos.html"), // tosUri + metadata, // metadata + postRedirectLogoutUris // postRedirectLogoutUris + }; + + for (int i = 0; i < FIELDS.length; i++) { + JsonObject createBody = new JsonObject(); + createBody.add(FIELDS[i], VALUES[i]); + + if ("postLogoutRedirectUris".equals(FIELDS[i])) { + createBody.add("redirectUris", redirectUris); + } + + JsonObject resp = createClient(process.getProcess(), createBody); + assertEquals("Unable to create client with field: " + FIELDS[i], "OK", resp.get("status").getAsString()); + assertEquals("Value mismatch for field: " + FIELDS[i], VALUES[i], resp.get(FIELDS[i])); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private JsonObject createClient(Main main, JsonObject createClientBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/clients", createClientBody, 1000, 1000, null, + SemVer.v5_2.get(), ""); + return response; + } + + private void verifyStructure(JsonObject response) throws Exception { + assertEquals(27, response.entrySet().size()); + String[] FIELDS = new String[]{ + "clientId", + "clientSecret", + "clientName", + "scope", + "redirectUris", + "enableRefreshTokenRotation", + "authorizationCodeGrantAccessTokenLifespan", + "authorizationCodeGrantIdTokenLifespan", + "authorizationCodeGrantRefreshTokenLifespan", + "clientCredentialsGrantAccessTokenLifespan", + "implicitGrantAccessTokenLifespan", + "implicitGrantIdTokenLifespan", + "refreshTokenGrantAccessTokenLifespan", + "refreshTokenGrantIdTokenLifespan", + "refreshTokenGrantRefreshTokenLifespan", + "tokenEndpointAuthMethod", + "clientUri", + "allowedCorsOrigins", + "audience", + "grantTypes", + "responseTypes", + "logoUri", + "policyUri", + "tosUri", + "createdAt", + "updatedAt", + "metadata" + }; + + for (String field : FIELDS) { + assertTrue(response.has(field)); + } + } +} diff --git a/src/test/java/io/supertokens/test/oauth/api/TestClientDelete5_2.java b/src/test/java/io/supertokens/test/oauth/api/TestClientDelete5_2.java new file mode 100644 index 000000000..523ee699f --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/TestClientDelete5_2.java @@ -0,0 +1,155 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +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.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.test.totp.TotpLicenseTest; +import io.supertokens.utils.SemVer; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +public class TestClientDelete5_2 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + OAuthAPIHelper.resetOAuthProvider(); + } + + @Test + public void testClientDelete() 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 }); + + Set clientIds = new HashSet<>(); + for (int i = 0; i < 10; i++) { + JsonObject client = createClient(process.getProcess()); + clientIds.add(client.get("clientId").getAsString()); + } + + String clientIdToDelete = clientIds.iterator().next(); + JsonObject resp = deleteClient(process.getProcess(), clientIdToDelete); + assertEquals("OK", resp.get("status").getAsString()); + assertEquals(true, resp.get("didExist").getAsBoolean()); + + JsonObject clients = listClients(process.getProcess()); + Set clientIdsAfterDeletion = new HashSet<>(); + for (JsonElement client : clients.get("clients").getAsJsonArray()) { + clientIdsAfterDeletion.add(client.getAsJsonObject().get("clientId").getAsString()); + } + + assertFalse(clientIdsAfterDeletion.contains(clientIdToDelete)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testClientDeleteWithInvalidClientId() 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 resp = deleteClient(process.getProcess(), "non-existent-client-id"); + assertEquals("OK", resp.get("status").getAsString()); + assertEquals(false, resp.get("didExist").getAsBoolean()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private JsonObject createClient(Main main) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/clients", new JsonObject(), 1000, 1000, null, + SemVer.v5_2.get(), ""); + return response; + } + + private JsonObject deleteClient(Main main, String clientId) throws Exception { + JsonObject body = new JsonObject(); + body.addProperty("clientId", clientId); + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/clients/remove", body, 1000, 1000, null, + SemVer.v5_2.get(), ""); + return response; + } + + private JsonObject listClients(Main main) throws Exception { + JsonObject response = HttpRequestForTesting.sendGETRequest(main, "", + "http://localhost:3567/recipe/oauth/clients/list", new HashMap<>(), 1000, 1000, null, + SemVer.v5_2.get(), ""); + return response; + } +} diff --git a/src/test/java/io/supertokens/test/oauth/api/TestClientList5_2.java b/src/test/java/io/supertokens/test/oauth/api/TestClientList5_2.java new file mode 100644 index 000000000..e94d878c4 --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/TestClientList5_2.java @@ -0,0 +1,232 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +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.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.test.totp.TotpLicenseTest; +import io.supertokens.utils.SemVer; +import java.util.Map; +import java.util.HashSet; +import java.util.Set; +import java.util.HashMap; + +public class TestClientList5_2 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + OAuthAPIHelper.resetOAuthProvider(); + } + + @Test + public void testClientList() 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}); + + Set clientIds = new HashSet<>(); + for (int i = 0; i < 10; i++) { + JsonObject client = createClient(process.getProcess(), new JsonObject()); + clientIds.add(client.get("clientId").getAsString()); + } + + JsonObject response = listClients(process.getProcess(), new HashMap<>()); + assertEquals(10, response.get("clients").getAsJsonArray().size()); + + Set clientIdsFromResponse = new HashSet<>(); + for (JsonElement client : response.get("clients").getAsJsonArray()) { + clientIdsFromResponse.add(client.getAsJsonObject().get("clientId").getAsString()); + } + assertEquals(clientIds, clientIdsFromResponse); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testClientListWithPagination() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH}); + + Set clientIds = new HashSet<>(); + for (int i = 0; i < 100; i++) { + JsonObject client = createClient(process.getProcess(), new JsonObject()); + clientIds.add(client.get("clientId").getAsString()); + } + + Map queryParams = new HashMap<>(); + queryParams.put("pageSize", "10"); + + Set clientIdsFromResponse = new HashSet<>(); + + JsonObject response = listClients(process.getProcess(), queryParams); + assertEquals(10, response.get("clients").getAsJsonArray().size()); + + for (JsonElement client : response.get("clients").getAsJsonArray()) { + clientIdsFromResponse.add(client.getAsJsonObject().get("clientId").getAsString()); + } + + while (response.has("nextPaginationToken")) { + queryParams.put("pageToken", response.get("nextPaginationToken").getAsString()); + + response = listClients(process.getProcess(), queryParams); + for (JsonElement client : response.get("clients").getAsJsonArray()) { + clientIdsFromResponse.add(client.getAsJsonObject().get("clientId").getAsString()); + } + } + + assertEquals(clientIds, clientIdsFromResponse); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testClientListWithClientNameFilter() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH}); + + Set clientIds = new HashSet<>(); + for (int i = 0; i < 100; i++) { + JsonObject clientBody = new JsonObject(); + clientBody.add("clientName", new JsonPrimitive("Hello")); + JsonObject client = createClient(process.getProcess(), clientBody); + clientIds.add(client.get("clientId").getAsString()); + } + + for (int i = 0; i < 100; i++) { + JsonObject clientBody = new JsonObject(); + clientBody.add("clientName", new JsonPrimitive("World")); + createClient(process.getProcess(), clientBody); + } + + Map queryParams = new HashMap<>(); + queryParams.put("pageSize", "10"); + queryParams.put("clientName", "Hello"); + + Set clientIdsFromResponse = new HashSet<>(); + + JsonObject response = listClients(process.getProcess(), queryParams); + assertEquals(10, response.get("clients").getAsJsonArray().size()); + + for (JsonElement client : response.get("clients").getAsJsonArray()) { + clientIdsFromResponse.add(client.getAsJsonObject().get("clientId").getAsString()); + } + + while (response.has("nextPaginationToken")) { + queryParams.put("pageToken", response.get("nextPaginationToken").getAsString()); + + response = listClients(process.getProcess(), queryParams); + for (JsonElement client : response.get("clients").getAsJsonArray()) { + clientIdsFromResponse.add(client.getAsJsonObject().get("clientId").getAsString()); + } + } + + assertEquals(clientIds, clientIdsFromResponse); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private JsonObject createClient(Main main, JsonObject createClientBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/clients", createClientBody, 1000, 1000, null, + SemVer.v5_2.get(), ""); + return response; + } + + private JsonObject listClients(Main main, Map queryParams) throws Exception { + JsonObject response = HttpRequestForTesting.sendGETRequest(main, "", + "http://localhost:3567/recipe/oauth/clients/list", queryParams, 1000, 1000, null, + SemVer.v5_2.get(), ""); + return response; + } +} diff --git a/src/test/java/io/supertokens/test/oauth/api/TestClientUpdate5_2.java b/src/test/java/io/supertokens/test/oauth/api/TestClientUpdate5_2.java new file mode 100644 index 000000000..50ac96d52 --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/TestClientUpdate5_2.java @@ -0,0 +1,203 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +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.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.test.totp.TotpLicenseTest; +import io.supertokens.utils.SemVer; + +public class TestClientUpdate5_2 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + OAuthAPIHelper.resetOAuthProvider(); + } + + @Test + public void testAllFields() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { EE_FEATURES.OAUTH }); + + String[] FIELDS = new String[] { + "clientSecret", + "clientName", + "scope", + "redirectUris", + "enableRefreshTokenRotation", + "authorizationCodeGrantAccessTokenLifespan", + "authorizationCodeGrantIdTokenLifespan", + "authorizationCodeGrantRefreshTokenLifespan", + "clientCredentialsGrantAccessTokenLifespan", + "implicitGrantAccessTokenLifespan", + "implicitGrantIdTokenLifespan", + "refreshTokenGrantAccessTokenLifespan", + "refreshTokenGrantIdTokenLifespan", + "refreshTokenGrantRefreshTokenLifespan", + "tokenEndpointAuthMethod", + "clientUri", + "allowedCorsOrigins", + "audience", + "grantTypes", + "responseTypes", + "logoUri", + "policyUri", + "tosUri", + "metadata", + "postLogoutRedirectUris" + }; + + JsonArray redirectUris = new JsonArray(); + redirectUris.add(new JsonPrimitive("http://localhost:3000/auth")); + + JsonArray allowedCorsOrigins = new JsonArray(); + allowedCorsOrigins.add(new JsonPrimitive("http://localhost:3000")); + + JsonArray audience = new JsonArray(); + audience.add(new JsonPrimitive("https://api.example.com")); + + JsonArray grantTypes = new JsonArray(); + grantTypes.add(new JsonPrimitive("authorization_code")); + grantTypes.add(new JsonPrimitive("implicit")); + grantTypes.add(new JsonPrimitive("refresh_token")); + grantTypes.add(new JsonPrimitive("client_credentials")); + + JsonArray responseTypes = new JsonArray(); + responseTypes.add(new JsonPrimitive("code")); + responseTypes.add(new JsonPrimitive("token")); + responseTypes.add(new JsonPrimitive("code token")); + responseTypes.add(new JsonPrimitive("id_token")); + responseTypes.add(new JsonPrimitive("code id_token")); + responseTypes.add(new JsonPrimitive("token id_token")); + responseTypes.add(new JsonPrimitive("code token id_token")); + + JsonArray postRedirectLogoutUris = new JsonArray(); + postRedirectLogoutUris.add(new JsonPrimitive("http://localhost:3000/logout")); + + JsonObject metadata = new JsonObject(); + metadata.addProperty("sub", "test-client-id"); + metadata.addProperty("iss", "http://localhost:3000"); + metadata.addProperty("aud", "https://api.example.com"); + metadata.addProperty("exp", 1714857600); + metadata.addProperty("iat", 1714857600); + metadata.addProperty("jti", "test-jti"); + metadata.addProperty("nonce", "test-nonce"); + + JsonElement[] VALUES = new JsonElement[] { + new JsonPrimitive("kEdQVPNLsl_FHOFBO_nWnj7P3."), // clientSecret + new JsonPrimitive("Test Client"), // clientName + new JsonPrimitive("offline_access offline openid"), // scope + redirectUris, // redirectUris + new JsonPrimitive(true), // enableRefreshTokenRotation + new JsonPrimitive("1h0m0s"), // authorizationCodeGrantAccessTokenLifespan + new JsonPrimitive("2h0m0s"), // authorizationCodeGrantIdTokenLifespan + new JsonPrimitive("3h0m0s"), // authorizationCodeGrantRefreshTokenLifespan + new JsonPrimitive("4h0m0s"), // clientCredentialsGrantAccessTokenLifespan + new JsonPrimitive("5h0m0s"), // implicitGrantAccessTokenLifespan + new JsonPrimitive("6h0m0s"), // implicitGrantIdTokenLifespan + new JsonPrimitive("7h0m0s"), // refreshTokenGrantAccessTokenLifespan + new JsonPrimitive("8h0m0s"), // refreshTokenGrantIdTokenLifespan + new JsonPrimitive("9h0m0s"), // refreshTokenGrantRefreshTokenLifespan + new JsonPrimitive("client_secret_post"), // tokenEndpointAuthMethod + new JsonPrimitive("http://localhost:3000"), // clientUri + allowedCorsOrigins, // allowedCorsOrigins + audience, // audience + grantTypes, // grantTypes + responseTypes, // responseTypes + new JsonPrimitive("http://localhost:3000/logo.png"), // logoUri + new JsonPrimitive("http://localhost:3000/policy.html"), // policyUri + new JsonPrimitive("http://localhost:3000/tos.html"), // tosUri + metadata, // metadata + postRedirectLogoutUris // postRedirectLogoutUris + }; + + JsonObject client = createClient(process.getProcess(), new JsonObject()); + + for (int i = 0; i < FIELDS.length; i++) { + JsonObject createBody = new JsonObject(); + createBody.add("clientId", client.get("clientId")); + createBody.add(FIELDS[i], VALUES[i]); + + JsonObject resp = updateClient(process.getProcess(), createBody); + assertEquals("Unable to create client with field: " + FIELDS[i], "OK", resp.get("status").getAsString()); + assertEquals("Value mismatch for field: " + FIELDS[i], VALUES[i], resp.get(FIELDS[i])); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private JsonObject createClient(Main main, JsonObject createClientBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/clients", createClientBody, 1000, 1000, null, + SemVer.v5_2.get(), ""); + return response; + } + + private JsonObject updateClient(Main main, JsonObject createClientBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(main, "", + "http://localhost:3567/recipe/oauth/clients", createClientBody, 1000, 1000, null, + SemVer.v5_2.get(), ""); + return response; + } +} diff --git a/src/test/java/io/supertokens/test/oauth/api/TestImplicitFlow.java b/src/test/java/io/supertokens/test/oauth/api/TestImplicitFlow.java new file mode 100644 index 000000000..21e6a1133 --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/TestImplicitFlow.java @@ -0,0 +1,204 @@ +/* + * 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 io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.totp.TotpLicenseTest; +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 static org.junit.Assert.assertNotNull; + +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +public class TestImplicitFlow { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + OAuthAPIHelper.resetOAuthProvider(); + } + + @Test + public void testImplicitGrantFlow() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH}); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject clientBody = new JsonObject(); + JsonArray grantTypes = new JsonArray(); + grantTypes.add(new JsonPrimitive("implicit")); + clientBody.add("grantTypes", grantTypes); + JsonArray responseTypes = new JsonArray(); + responseTypes.add(new JsonPrimitive("token")); + 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 profile email"); + + JsonObject client = OAuthAPIHelper.createClient(process.getProcess(), clientBody); + + 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", "token"); + params.addProperty("scope", "openid profile email"); + params.addProperty("state", "test12345678"); + + authRequestBody.add("params", params); + + JsonObject authResponse = OAuthAPIHelper.auth(process.getProcess(), 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", "someuserid"); + acceptLoginRequestBody.addProperty("remember", true); + acceptLoginRequestBody.addProperty("rememberFor", 3600); + + JsonObject acceptLoginRequestResponse = OAuthAPIHelper.acceptLoginRequest(process.getProcess(), 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(process.getProcess(), 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("remember", true); + acceptConsentRequestBody.addProperty("rememberFor", 3600); + acceptConsentRequestBody.addProperty("iss", "http://localhost:3001/auth"); + acceptConsentRequestBody.addProperty("tId", "public"); + acceptConsentRequestBody.addProperty("rsub", "someuser"); + acceptConsentRequestBody.addProperty("sessionHandle", "session-handle"); + acceptConsentRequestBody.add("initialAccessTokenPayload", new JsonObject()); + acceptConsentRequestBody.add("initialIdTokenPayload", new JsonObject()); + + queryParams = new HashMap<>(); + queryParams.put("consentChallenge", consentChallenge); + + JsonObject acceptConsentRequestResponse = OAuthAPIHelper.acceptConsentRequest(process.getProcess(), 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(process.getProcess(), authRequestBody); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + // Helper method to split query parameters + 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; + } +} 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/TestLoginRequest5_2.java b/src/test/java/io/supertokens/test/oauth/api/TestLoginRequest5_2.java new file mode 100644 index 000000000..be54fc4ba --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/TestLoginRequest5_2.java @@ -0,0 +1,501 @@ +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.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +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.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.utils.SemVer; + +public class TestLoginRequest5_2 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + OAuthAPIHelper.resetOAuthProvider(); + } + + @Test + public void testLoginRequestCreationAndGet() 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()); + String clientId = client.get("clientId").getAsString(); + + JsonObject authResponse = authRequest(process.getProcess(), clientId); + + assertEquals("OK", authResponse.get("status").getAsString()); + assertEquals(3, authResponse.entrySet().size()); + assertTrue(authResponse.has("redirectTo")); + assertTrue(authResponse.has("cookies")); + + String cookies = authResponse.get("cookies").getAsJsonArray().get(0).getAsString(); + cookies = cookies.split(";")[0]; + assertTrue(cookies.startsWith("st_oauth_login_csrf_dev_")); + + String redirectTo = authResponse.get("redirectTo").getAsString(); + assertTrue(redirectTo.startsWith("{apiDomain}")); + + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001"); + Map queryParams = Utils.splitQueryString(new URL(redirectTo).getQuery()); + assertEquals(1, queryParams.size()); + + String loginChallenge = queryParams.get("login_challenge"); + assertNotNull(loginChallenge); + + JsonObject loginRequestResponse = getLoginRequest(process.getProcess(), loginChallenge); + + assertEquals(10, loginRequestResponse.entrySet().size()); + assertEquals("OK", loginRequestResponse.get("status").getAsString()); + assertTrue(loginRequestResponse.has("challenge")); + assertTrue(loginRequestResponse.has("requestedScope")); + assertTrue(loginRequestResponse.has("requestedAccessTokenAudience")); + assertFalse(loginRequestResponse.get("skip").getAsBoolean()); + assertEquals("", loginRequestResponse.get("subject").getAsString()); + assertTrue(loginRequestResponse.has("oidcContext")); + assertTrue(loginRequestResponse.has("client")); + assertTrue(loginRequestResponse.has("requestUrl")); + assertTrue(loginRequestResponse.has("sessionId")); + + JsonArray requestedScope = loginRequestResponse.getAsJsonArray("requestedScope"); + assertEquals(2, requestedScope.size()); + assertTrue(requestedScope.contains(new JsonPrimitive("openid"))); + assertTrue(requestedScope.contains(new JsonPrimitive("offline_access"))); + + JsonArray requestedAccessTokenAudience = loginRequestResponse.getAsJsonArray("requestedAccessTokenAudience"); + assertEquals(0, requestedAccessTokenAudience.size()); + + JsonObject clientInResponse = loginRequestResponse.getAsJsonObject("client"); + verifyClientStructure(clientInResponse); + + assertTrue(loginRequestResponse.get("requestUrl").getAsString().startsWith("http://localhost:4444/oauth2/auth?")); + assertTrue(loginRequestResponse.has("sessionId")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testLoginRequestGetWithDeletedClient() 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()); + String clientId = client.get("clientId").getAsString(); + + JsonObject authResponse = authRequest(process.getProcess(), clientId); + + assertEquals("OK", authResponse.get("status").getAsString()); + assertEquals(3, authResponse.entrySet().size()); + assertTrue(authResponse.has("redirectTo")); + assertTrue(authResponse.has("cookies")); + + String cookies = authResponse.get("cookies").getAsJsonArray().get(0).getAsString(); + cookies = cookies.split(";")[0]; + assertTrue(cookies.startsWith("st_oauth_login_csrf_dev_")); + + String redirectTo = authResponse.get("redirectTo").getAsString(); + assertTrue(redirectTo.startsWith("{apiDomain}")); + + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001"); + Map queryParams = Utils.splitQueryString(new URL(redirectTo).getQuery()); + assertEquals(1, queryParams.size()); + + String loginChallenge = queryParams.get("login_challenge"); + assertNotNull(loginChallenge); + + deleteClient(process.getProcess(), clientId); + + JsonObject loginRequestResponse = getLoginRequest(process.getProcess(), loginChallenge); + + assertEquals("CLIENT_NOT_FOUND_ERROR", loginRequestResponse.get("status").getAsString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testAcceptLoginRequest() 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()); + String clientId = client.get("clientId").getAsString(); + + JsonObject authResponse = authRequest(process.getProcess(), clientId); + + assertEquals("OK", authResponse.get("status").getAsString()); + assertEquals(3, authResponse.entrySet().size()); + assertTrue(authResponse.has("redirectTo")); + assertTrue(authResponse.has("cookies")); + + String cookies = authResponse.get("cookies").getAsJsonArray().get(0).getAsString(); + cookies = cookies.split(";")[0]; + assertTrue(cookies.startsWith("st_oauth_login_csrf_dev_")); + + String redirectTo = authResponse.get("redirectTo").getAsString(); + assertTrue(redirectTo.startsWith("{apiDomain}")); + + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001"); + Map queryParams = Utils.splitQueryString(new URL(redirectTo).getQuery()); + assertEquals(1, queryParams.size()); + + String loginChallenge = queryParams.get("login_challenge"); + assertNotNull(loginChallenge); + + JsonObject acceptResponse = acceptLoginRequest(process.getProcess(), loginChallenge); + + assertEquals("OK", acceptResponse.get("status").getAsString()); + + redirectTo = acceptResponse.get("redirectTo").getAsString(); + assertTrue(redirectTo.startsWith("{apiDomain}")); + + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001"); + queryParams = Utils.splitQueryString(new URL(redirectTo).getQuery()); + assertEquals(6, queryParams.size()); + + String loginVerifier = queryParams.get("login_verifier"); + assertNotNull(loginVerifier); + + String expectedRedirectUri = "http://localhost.com:3000/auth/callback/supertokens"; + String expectedResponseType = "code"; + String expectedScope = "openid offline_access"; + String expectedState = "test12345678"; + + assertEquals(clientId, queryParams.get("client_id")); + assertEquals(expectedRedirectUri, queryParams.get("redirect_uri")); + assertEquals(expectedResponseType, queryParams.get("response_type")); + assertEquals(expectedScope, queryParams.get("scope")); + assertEquals(expectedState, queryParams.get("state")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testAcceptNonExistantLoginRequest() 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()); + String clientId = client.get("clientId").getAsString(); + + JsonObject authResponse = authRequest(process.getProcess(), clientId); + + assertEquals("OK", authResponse.get("status").getAsString()); + assertEquals(3, authResponse.entrySet().size()); + assertTrue(authResponse.has("redirectTo")); + assertTrue(authResponse.has("cookies")); + + String cookies = authResponse.get("cookies").getAsJsonArray().get(0).getAsString(); + cookies = cookies.split(";")[0]; + assertTrue(cookies.startsWith("st_oauth_login_csrf_dev_")); + + String redirectTo = authResponse.get("redirectTo").getAsString(); + assertTrue(redirectTo.startsWith("{apiDomain}")); + + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001"); + Map queryParams = Utils.splitQueryString(new URL(redirectTo).getQuery()); + assertEquals(1, queryParams.size()); + + String loginChallenge = queryParams.get("login_challenge"); + assertNotNull(loginChallenge); + + JsonObject acceptResponse = acceptLoginRequest(process.getProcess(), loginChallenge + "extras"); + + assertEquals("OAUTH_ERROR", acceptResponse.get("status").getAsString()); + assertEquals("Not Found", acceptResponse.get("error").getAsString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testRejectLoginRequest() 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()); + String clientId = client.get("clientId").getAsString(); + + JsonObject authResponse = authRequest(process.getProcess(), clientId); + + assertEquals("OK", authResponse.get("status").getAsString()); + assertEquals(3, authResponse.entrySet().size()); + assertTrue(authResponse.has("redirectTo")); + assertTrue(authResponse.has("cookies")); + + String cookies = authResponse.get("cookies").getAsJsonArray().get(0).getAsString(); + cookies = cookies.split(";")[0]; + assertTrue(cookies.startsWith("st_oauth_login_csrf_dev_")); + + String redirectTo = authResponse.get("redirectTo").getAsString(); + assertTrue(redirectTo.startsWith("{apiDomain}")); + + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001"); + Map queryParams = Utils.splitQueryString(new URL(redirectTo).getQuery()); + assertEquals(1, queryParams.size()); + + String loginChallenge = queryParams.get("login_challenge"); + assertNotNull(loginChallenge); + + JsonObject rejectResponse = rejectLoginRequest(process.getProcess(), loginChallenge); + + assertEquals("OK", rejectResponse.get("status").getAsString()); + redirectTo = rejectResponse.get("redirectTo").getAsString(); + assertTrue(redirectTo.startsWith("{apiDomain}")); + + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001"); + queryParams = Utils.splitQueryString(new URL(redirectTo).getQuery()); + assertEquals(6, queryParams.size()); + + assertTrue(queryParams.containsKey("client_id")); + assertTrue(queryParams.containsKey("login_verifier")); + assertTrue(queryParams.containsKey("redirect_uri")); + assertTrue(queryParams.containsKey("response_type")); + assertTrue(queryParams.containsKey("scope")); + assertTrue(queryParams.containsKey("state")); + + assertEquals(clientId, queryParams.get("client_id")); + assertEquals("http://localhost.com:3000/auth/callback/supertokens", queryParams.get("redirect_uri")); + assertEquals("code", queryParams.get("response_type")); + assertEquals("openid offline_access", queryParams.get("scope")); + assertEquals("test12345678", queryParams.get("state")); + + // Verify login_verifier is present and not empty + assertNotNull(queryParams.get("login_verifier")); + assertFalse(queryParams.get("login_verifier").isEmpty()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private JsonObject acceptLoginRequest(Main main, String loginChallenge) throws Exception { + Map acceptLoginRequestParams = new HashMap<>(); + acceptLoginRequestParams.put("loginChallenge", loginChallenge); + + JsonObject acceptLoginRequestBody = new JsonObject(); + acceptLoginRequestBody.addProperty("subject", "someuserid"); + acceptLoginRequestBody.addProperty("remember", true); + acceptLoginRequestBody.addProperty("rememberFor", 3600); + acceptLoginRequestBody.addProperty("identityProviderSessionId", "session-handle"); + + String url = "http://localhost:3567/recipe/oauth/auth/requests/login/accept?loginChallenge=" + URLEncoder.encode(loginChallenge, StandardCharsets.UTF_8.toString()); + + JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(main, "", + url, acceptLoginRequestBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + private JsonObject rejectLoginRequest(Main main, String loginChallenge) throws Exception { + Map rejectLoginRequestParams = new HashMap<>(); + rejectLoginRequestParams.put("loginChallenge", loginChallenge); + + JsonObject acceptLoginRequestBody = new JsonObject(); + + String url = "http://localhost:3567/recipe/oauth/auth/requests/login/reject?loginChallenge=" + URLEncoder.encode(loginChallenge, StandardCharsets.UTF_8.toString()); + + JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(main, "", + url, acceptLoginRequestBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + private JsonObject createClient(Main main) throws Exception { + JsonObject createClientBody = new JsonObject(); + JsonArray grantTypes = new JsonArray(); + grantTypes.add(new JsonPrimitive("authorization_code")); + grantTypes.add(new JsonPrimitive("refresh_token")); + createClientBody.add("grantTypes", grantTypes); + JsonArray responseTypes = new JsonArray(); + responseTypes.add(new JsonPrimitive("code")); + responseTypes.add(new JsonPrimitive("id_token")); + createClientBody.add("responseTypes", responseTypes); + JsonArray redirectUris = new JsonArray(); + redirectUris.add(new JsonPrimitive("http://localhost.com:3000/auth/callback/supertokens")); + createClientBody.add("redirectUris", redirectUris); + createClientBody.addProperty("scope", "openid email offline_access"); + createClientBody.addProperty("tokenEndpointAuthMethod", "client_secret_post"); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/clients", createClientBody, 1000, 1000, null, + SemVer.v5_2.get(), ""); + return response; + } + + private JsonObject authRequest(Main main, String clientId) throws Exception { + JsonObject authRequestBody = new JsonObject(); + JsonObject params = new JsonObject(); + params.addProperty("client_id", clientId); + 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 response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/auth", authRequestBody, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + private JsonObject getLoginRequest(Main main, String loginChallenge) throws Exception { + Map queryParams = new HashMap<>(); + queryParams.put("loginChallenge", loginChallenge); + JsonObject response = HttpRequestForTesting.sendGETRequest(main, "", + "http://localhost:3567/recipe/oauth/auth/requests/login", queryParams, 5000, 5000, null, + SemVer.v5_2.get(), ""); + return response; + } + + private JsonObject deleteClient(Main main, String clientId) throws Exception { + JsonObject body = new JsonObject(); + body.addProperty("clientId", clientId); + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/clients/remove", body, 1000, 1000, null, + SemVer.v5_2.get(), ""); + return response; + } + + private void verifyClientStructure(JsonObject response) throws Exception { + assertEquals(27, response.entrySet().size()); + String[] FIELDS = new String[]{ + "clientId", + "clientSecret", + "clientName", + "scope", + "redirectUris", + "enableRefreshTokenRotation", + "authorizationCodeGrantAccessTokenLifespan", + "authorizationCodeGrantIdTokenLifespan", + "authorizationCodeGrantRefreshTokenLifespan", + "clientCredentialsGrantAccessTokenLifespan", + "implicitGrantAccessTokenLifespan", + "implicitGrantIdTokenLifespan", + "refreshTokenGrantAccessTokenLifespan", + "refreshTokenGrantIdTokenLifespan", + "refreshTokenGrantRefreshTokenLifespan", + "tokenEndpointAuthMethod", + "clientUri", + "allowedCorsOrigins", + "audience", + "grantTypes", + "responseTypes", + "logoUri", + "policyUri", + "tosUri", + "createdAt", + "updatedAt", + "metadata" + }; + + for (String field : FIELDS) { + assertTrue(response.has(field)); + } + } + +} diff --git a/src/test/java/io/supertokens/test/oauth/api/TestRefreshTokenFlowWithTokenRotationOptions.java b/src/test/java/io/supertokens/test/oauth/api/TestRefreshTokenFlowWithTokenRotationOptions.java new file mode 100644 index 000000000..967862d1b --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/TestRefreshTokenFlowWithTokenRotationOptions.java @@ -0,0 +1,426 @@ +/* + * 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.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.totp.TotpLicenseTest; +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 TestRefreshTokenFlowWithTokenRotationOptions { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + OAuthAPIHelper.resetOAuthProvider(); + } + + private static JsonObject createClient(Main main, boolean enableRefreshTokenRotation) 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"); + clientBody.addProperty("enableRefreshTokenRotation", enableRefreshTokenRotation); + + return OAuthAPIHelper.createClient(main, clientBody); + } + + private static void updateClient(Main main, JsonObject client, boolean enableRefreshTokenRotation) throws Exception { + JsonObject updateClientBody = new JsonObject(); + updateClientBody.addProperty("clientId", client.get("clientId").getAsString()); + updateClientBody.addProperty("enableRefreshTokenRotation", enableRefreshTokenRotation); + OAuthAPIHelper.updateClient(main, updateClientBody); + } + + private static JsonObject completeFlowAndGetTokens(Main main, JsonObject client) 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", "someuserid"); + acceptLoginRequestBody.addProperty("remember", true); + acceptLoginRequestBody.addProperty("rememberFor", 3600); + acceptLoginRequestBody.addProperty("identityProviderSessionId", "session-handle"); + + 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", "someuserid"); + acceptConsentRequestBody.addProperty("sessionHandle", "session-handle"); + 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(); +// JsonObject accessToken = new JsonObject(); +// accessToken.addProperty("gid", "gidForTesting"); + 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"); + + return OAuthAPIHelper.token(main, tokenRequestBody); + + } + + private static JsonObject refreshToken(Main main, JsonObject client, String refreshToken) throws Exception { + JsonObject tokenRequestBody = new JsonObject(); + 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()); + + tokenRequestBody.add("access_token", new JsonObject()); + tokenRequestBody.add("id_token", new JsonObject()); + tokenRequestBody.add("inputBody", inputBody); + tokenRequestBody.addProperty("iss", "http://localhost:3001/auth"); + + return OAuthAPIHelper.token(main, tokenRequestBody); + } + + @Test + public void testRefreshTokenWithRotationDisabled() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH}); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject client = createClient(process.getProcess(), false); + JsonObject tokens = completeFlowAndGetTokens(process.getProcess(), client); + + String refreshToken = tokens.get("refresh_token").getAsString(); + JsonObject newTokens = refreshToken(process.getProcess(), client, refreshToken); + assertFalse(newTokens.has("refresh_token")); + + // refresh again with original refresh token + newTokens = refreshToken(process.getProcess(), client, refreshToken); + assertFalse(newTokens.has("refresh_token")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testRefreshTokenWithRotationEnabled() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH}); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject client = createClient(process.getProcess(), true); + JsonObject tokens = completeFlowAndGetTokens(process.getProcess(), client); + + String refreshToken = tokens.get("refresh_token").getAsString(); + JsonObject newTokens = refreshToken(process.getProcess(), client, refreshToken); + assertTrue(newTokens.has("refresh_token")); + + String newRefreshToken = newTokens.get("refresh_token").getAsString(); + + // refresh again with original refresh token + newTokens = refreshToken(process.getProcess(), client, refreshToken); + 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")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testRefreshTokenWhenRotationIsEnabledAfter() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH}); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject client = createClient(process.getProcess(), false); + JsonObject tokens = completeFlowAndGetTokens(process.getProcess(), client); + + String refreshToken = tokens.get("refresh_token").getAsString(); + JsonObject newTokens = refreshToken(process.getProcess(), client, refreshToken); + assertFalse(newTokens.has("refresh_token")); + + updateClient(process.getProcess(), client, true); + + newTokens = refreshToken(process.getProcess(), client, refreshToken); + assertTrue(newTokens.has("refresh_token")); + + String newRefreshToken = newTokens.get("refresh_token").getAsString(); + + // refresh again with original refresh token + newTokens = refreshToken(process.getProcess(), client, refreshToken); + 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")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testRefreshTokenWithRotationIsDisabledAfter() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH}); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject client = createClient(process.getProcess(), true); + JsonObject tokens = completeFlowAndGetTokens(process.getProcess(), client); + + String refreshToken = tokens.get("refresh_token").getAsString(); + JsonObject newTokens = refreshToken(process.getProcess(), client, refreshToken); + assertTrue(newTokens.has("refresh_token")); + + String newRefreshToken = newTokens.get("refresh_token").getAsString(); + + updateClient(process.getProcess(), client, false); + + 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")); + + newTokens = refreshToken(process.getProcess(), client, newRefreshToken); + assertFalse(newTokens.has("refresh_token")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + 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; + } +} 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 new file mode 100644 index 000000000..c0eece257 --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/TestRevoke5_2.java @@ -0,0 +1,497 @@ +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.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 + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + OAuthAPIHelper.resetOAuthProvider(); + } + + @Test + public void testRevokeAccessToken() 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(); + + Thread.sleep(500); + JsonObject revokeResponse = revokeToken(process.getProcess(), null, accessToken); + assertEquals("OK", revokeResponse.get("status").getAsString()); + + Thread.sleep(500); + // test introspect access token + JsonObject introspectResponse = introspectToken(process.getProcess(), + tokenResponse.get("access_token").getAsString()); + assertEquals("OK", introspectResponse.get("status").getAsString()); + assertFalse(introspectResponse.get("active").getAsBoolean()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testRevokeRefreshToken() 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 refreshToken = tokenResponse.get("refresh_token").getAsString(); + + Thread.sleep(500); + JsonObject revokeResponse = revokeToken(process.getProcess(), client, refreshToken); + assertEquals("OK", revokeResponse.get("status").getAsString()); + + Thread.sleep(500); + + // test introspect refresh token + JsonObject introspectResponse = introspectToken(process.getProcess(), refreshToken); + assertEquals("OK", introspectResponse.get("status").getAsString()); + assertFalse(introspectResponse.get("active").getAsBoolean()); + + // test introspect access token + introspectResponse = introspectToken(process.getProcess(), tokenResponse.get("access_token").getAsString()); + assertEquals("OK", introspectResponse.get("status").getAsString()); + assertFalse(introspectResponse.get("active").getAsBoolean()); + + // test refresh token + JsonObject refreshResponse = refreshToken(process.getProcess(), client, refreshToken); + assertEquals("OAUTH_ERROR", refreshResponse.get("status").getAsString()); + assertEquals("token_inactive", refreshResponse.get("error").getAsString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testRevokeClientId() 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); + + // revoke client id + JsonObject revokeClientIdResponse = revokeClientId(process.getProcess(), client); + assertEquals("OK", revokeClientIdResponse.get("status").getAsString()); + + Thread.sleep(1000); + + // 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()); + 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 (not allowed) + JsonObject refreshResponse = refreshToken(process.getProcess(), client, + tokenResponse.get("refresh_token").getAsString()); + assertEquals("OAUTH_ERROR", refreshResponse.get("status").getAsString()); + + Thread.sleep(1000); + + // issue new tokens + tokenResponse = issueTokens(process.getProcess(), client, user.getSupertokensUserId(), + user.getSupertokensUserId(), session.session.handle); + + // test introspect refresh token (allowed) + introspectResponse = introspectToken(process.getProcess(), + tokenResponse.get("refresh_token").getAsString()); + assertEquals("OK", introspectResponse.get("status").getAsString()); + assertTrue(introspectResponse.get("active").getAsBoolean()); + + // test introspect access token (allowed) + introspectResponse = introspectToken(process.getProcess(), tokenResponse.get("access_token").getAsString()); + assertEquals("OK", introspectResponse.get("status").getAsString()); + assertTrue(introspectResponse.get("active").getAsBoolean()); + + // test refresh token (allowed) + refreshResponse = refreshToken(process.getProcess(), client, + tokenResponse.get("refresh_token").getAsString()); + assertEquals("OK", refreshResponse.get("status").getAsString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private JsonObject revokeClientId(Main process, JsonObject client) throws Exception { + JsonObject revokeClientIdRequestBody = new JsonObject(); + revokeClientIdRequestBody.addProperty("client_id", client.get("clientId").getAsString()); + return OAuthAPIHelper.revokeClientId(process, revokeClientIdRequestBody); + } + + @Test + public void testRevokeSessionHandle() 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); + + // revoke client id + JsonObject revokeSessionhandleResponse = revokeSessionHandle(process.getProcess(), session.session.handle); + System.out.println(revokeSessionhandleResponse.toString()); + assertEquals("OK", revokeSessionhandleResponse.get("status").getAsString()); + + Thread.sleep(1000); + + // test introspect refresh token (not allowed) + JsonObject introspectResponse = introspectToken(process.getProcess(), + tokenResponse.get("refresh_token").getAsString()); + assertEquals("OK", introspectResponse.get("status").getAsString()); + 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 (not allowed) + JsonObject refreshResponse = refreshToken(process.getProcess(), client, + tokenResponse.get("refresh_token").getAsString()); + assertEquals("OAUTH_ERROR", refreshResponse.get("status").getAsString()); + assertEquals("token_inactive", refreshResponse.get("error").getAsString()); + + Thread.sleep(1000); + + // issue new tokens + tokenResponse = issueTokens(process.getProcess(), client, user.getSupertokensUserId(), + user.getSupertokensUserId(), session.session.handle); + + // test introspect refresh token (allowed) + introspectResponse = introspectToken(process.getProcess(), + tokenResponse.get("refresh_token").getAsString()); + assertEquals("OK", introspectResponse.get("status").getAsString()); + assertTrue(introspectResponse.get("active").getAsBoolean()); + + // test introspect access token (allowed) + introspectResponse = introspectToken(process.getProcess(), tokenResponse.get("access_token").getAsString()); + assertEquals("OK", introspectResponse.get("status").getAsString()); + assertTrue(introspectResponse.get("active").getAsBoolean()); + + // test refresh token (allowed) + refreshResponse = refreshToken(process.getProcess(), client, + tokenResponse.get("refresh_token").getAsString()); + assertEquals("OK", refreshResponse.get("status").getAsString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private JsonObject revokeSessionHandle(Main process, String handle) throws Exception { + JsonObject revokeSessionHandleRequestBody = new JsonObject(); + revokeSessionHandleRequestBody.addProperty("sessionHandle", handle); + return OAuthAPIHelper.revokeSessionHandle(process, revokeSessionHandleRequestBody); + } + + 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; + } + + 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 revokeToken(Main main, JsonObject client, String token) throws Exception { + JsonObject revokeRequestBody = new JsonObject(); + revokeRequestBody.addProperty("token", token); + if (client != null) { + revokeRequestBody.addProperty("client_id", client.get("clientId").getAsString()); + revokeRequestBody.addProperty("client_secret", client.get("clientSecret").getAsString()); + } + return OAuthAPIHelper.revoke(main, revokeRequestBody); + } +}