diff --git a/config.yaml b/config.yaml index ec890e826..7dd967768 100644 --- a/config.yaml +++ b/config.yaml @@ -160,7 +160,7 @@ core_config_version: 0 # service. # oauth_provider_admin_service_url: -# (OPTIONAL | Default: http://localhost:3000) string value. If specified, the core uses this URL replace the default +# (OPTIONAL | Default: null) string value. If specified, the core uses this URL replace the default # consent and login URLs to {apiDomain}. # oauth_provider_consent_login_base_url: diff --git a/devConfig.yaml b/devConfig.yaml index 468752aa5..ed83e6bb5 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -154,17 +154,16 @@ disable_telemetry: true # (OPTIONAL | Default: null) string value. If specified, the core uses this URL to connect to the OAuth provider # public service. -# oauth_provider_public_service_url: +oauth_provider_public_service_url: http://localhost:4444 # (OPTIONAL | Default: null) string value. If specified, the core uses this URL to connect to the OAuth provider admin # service. -# oauth_provider_admin_service_url: +oauth_provider_admin_service_url: http://localhost:4445 - -# (OPTIONAL | Default: http://localhost:3000) string value. If specified, the core uses this URL replace the default +# (OPTIONAL | Default: null) string value. If specified, the core uses this URL replace the default # consent and login URLs to {apiDomain}. -# oauth_provider_consent_login_base_url: +oauth_provider_consent_login_base_url: http://localhost:4001/auth # (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_hydra: \ No newline at end of file +# oauth_provider_url_configured_in_hydra: diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index 4c959f6f8..e01b8ed26 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -24,6 +24,7 @@ import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.StorageUtils; import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; import io.supertokens.pluginInterface.dashboard.sqlStorage.DashboardSQLStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -32,6 +33,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.ThirdPartyConfig; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.utils.Utils; @@ -338,6 +340,28 @@ private JsonObject getAccountLinkingStats() throws StorageQueryException, Tenant return result; } + private JsonObject getOAuthStats() throws StorageQueryException, TenantOrAppNotFoundException { + JsonObject result = new JsonObject(); + + OAuthStorage oAuthStorage = StorageUtils.getOAuthStorage(StorageLayer.getStorage( + this.appIdentifier.getAsPublicTenantIdentifier(), main)); + + result.addProperty("totalNumberOfClients", oAuthStorage.countTotalNumberOfClientsForApp(appIdentifier)); + result.addProperty("numberOfClientCredentialsOnlyClients", oAuthStorage.countTotalNumberOfClientCredentialsOnlyClientsForApp(appIdentifier)); + result.addProperty("numberOfM2MTokensAlive", oAuthStorage.countTotalNumberOfM2MTokensAlive(appIdentifier)); + + long now = System.currentTimeMillis(); + JsonArray tokensCreatedArray = new JsonArray(); + for (int i = 1; i <= 31; i++) { + long timestamp = now - (i * 24 * 60 * 60 * 1000L); + int numberOfTokensCreated = oAuthStorage.countTotalNumberOfM2MTokensCreatedSince(this.appIdentifier, timestamp); + tokensCreatedArray.add(new JsonPrimitive(numberOfTokensCreated)); + } + result.add("numberOfM2MTokensCreated", tokensCreatedArray); + + return result; + } + private JsonArray getMAUs() throws StorageQueryException, TenantOrAppNotFoundException { JsonArray mauArr = new JsonArray(); long now = System.currentTimeMillis(); @@ -395,6 +419,10 @@ public JsonObject getPaidFeatureStats() throws StorageQueryException, TenantOrAp if (feature == EE_FEATURES.SECURITY) { usageStats.add(EE_FEATURES.SECURITY.toString(), new JsonObject()); } + + if (feature == EE_FEATURES.OAUTH) { + usageStats.add(EE_FEATURES.OAUTH.toString(), getOAuthStats()); + } } usageStats.add("maus", getMAUs()); diff --git a/src/main/java/io/supertokens/Main.java b/src/main/java/io/supertokens/Main.java index f6e9f29ea..1eef7e500 100644 --- a/src/main/java/io/supertokens/Main.java +++ b/src/main/java/io/supertokens/Main.java @@ -20,6 +20,7 @@ import io.supertokens.config.Config; import io.supertokens.config.CoreConfig; import io.supertokens.cronjobs.Cronjobs; +import io.supertokens.cronjobs.cleanupOAuthRevokeList.CleanupOAuthRevokeList; import io.supertokens.cronjobs.deleteExpiredAccessTokenSigningKeys.DeleteExpiredAccessTokenSigningKeys; import io.supertokens.cronjobs.deleteExpiredDashboardSessions.DeleteExpiredDashboardSessions; import io.supertokens.cronjobs.deleteExpiredEmailVerificationTokens.DeleteExpiredEmailVerificationTokens; @@ -256,6 +257,8 @@ private void init() throws IOException, StorageQueryException { // starts DeleteExpiredAccessTokenSigningKeys cronjob if the access token signing keys can change Cronjobs.addCronjob(this, DeleteExpiredAccessTokenSigningKeys.init(this, uniqueUserPoolIdsTenants)); + Cronjobs.addCronjob(this, CleanupOAuthRevokeList.init(this, uniqueUserPoolIdsTenants)); + // this is to ensure tenantInfos are in sync for the new cron job as well MultitenancyHelper.getInstance(this).refreshCronjobs(); diff --git a/src/main/java/io/supertokens/config/Config.java b/src/main/java/io/supertokens/config/Config.java index a51b8bd1d..91ed63af5 100644 --- a/src/main/java/io/supertokens/config/Config.java +++ b/src/main/java/io/supertokens/config/Config.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import io.supertokens.Main; diff --git a/src/main/java/io/supertokens/config/CoreConfig.java b/src/main/java/io/supertokens/config/CoreConfig.java index 1b82d78af..b82053248 100644 --- a/src/main/java/io/supertokens/config/CoreConfig.java +++ b/src/main/java/io/supertokens/config/CoreConfig.java @@ -297,17 +297,15 @@ public class CoreConfig { @JsonProperty @HideFromDashboard @ConfigDescription( - "If specified, the core uses this URL replace the default consent and login URLs to {apiDomain}. Defaults to 'http://localhost:3000'") - private String oauth_provider_consent_login_base_url = "http://localhost:3000"; + "If specified, the core uses this URL replace the default consent and login URLs to {apiDomain}. Defaults to 'null'") + private String oauth_provider_consent_login_base_url = null; @NotConflictingInApp @JsonProperty @HideFromDashboard @ConfigDescription( "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. Defaults to the oauth_provider_public_service_url") - private String oauth_provider_url_configured_in_hydra; - - + private String oauth_provider_url_configured_in_hydra = null; @ConfigYamlOnly @JsonProperty diff --git a/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java b/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java new file mode 100644 index 000000000..a94d65d51 --- /dev/null +++ b/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java @@ -0,0 +1,57 @@ +package io.supertokens.cronjobs.cleanupOAuthRevokeList; + +import java.util.List; + +import io.supertokens.Main; +import io.supertokens.cronjobs.CronTask; +import io.supertokens.cronjobs.CronTaskTest; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.StorageUtils; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.oauth.OAuthStorage; +import io.supertokens.storageLayer.StorageLayer; + +public class CleanupOAuthRevokeList extends CronTask { + + public static final String RESOURCE_KEY = "io.supertokens.cronjobs.cleanupOAuthRevokeList" + + ".CleanupOAuthRevokeList"; + + private CleanupOAuthRevokeList(Main main, List> tenantsInfo) { + super("CleanupOAuthRevokeList", main, tenantsInfo, true); + } + + public static CleanupOAuthRevokeList init(Main main, List> tenantsInfo) { + return (CleanupOAuthRevokeList) main.getResourceDistributor() + .setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY, + new CleanupOAuthRevokeList(main, tenantsInfo)); + } + + @Override + protected void doTaskPerApp(AppIdentifier app) throws Exception { + Storage storage = StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main); + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.cleanUpExpiredAndRevokedTokens(app); + } + + @Override + public int getIntervalTimeSeconds() { + if (Main.isTesting) { + Integer interval = CronTaskTest.getInstance(main).getIntervalInSeconds(RESOURCE_KEY); + if (interval != null) { + return interval; + } + } + // Every 24 hours. + return 24 * 3600; + } + + @Override + public int getInitialWaitTimeSeconds() { + if (!Main.isTesting) { + return getIntervalTimeSeconds(); + } else { + return 0; + } + } +} diff --git a/src/main/java/io/supertokens/httpRequest/HttpRequest.java b/src/main/java/io/supertokens/httpRequest/HttpRequest.java index 7edc8183f..b3f7e805d 100644 --- a/src/main/java/io/supertokens/httpRequest/HttpRequest.java +++ b/src/main/java/io/supertokens/httpRequest/HttpRequest.java @@ -16,17 +16,22 @@ package io.supertokens.httpRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; + import com.google.gson.JsonElement; import com.google.gson.JsonParser; -import io.supertokens.Main; -import java.io.*; -import java.net.*; -import java.net.http.HttpClient; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Map; +import io.supertokens.Main; public class HttpRequest { @@ -126,7 +131,7 @@ public static T sendGETRequest(Main main, String requestID, String url, Map< public static T sendGETRequestWithResponseHeaders(Main main, String requestID, String url, Map params, int connectionTimeoutMS, int readTimeoutMS, Integer version, - Map> responseHeaders, boolean followRedirects) + Map responseHeaders) throws IOException, HttpResponseException { StringBuilder paramBuilder = new StringBuilder(); @@ -152,12 +157,12 @@ public static T sendGETRequestWithResponseHeaders(Main main, String requestI if (version != null) { con.setRequestProperty("api-version", version + ""); } - con.setInstanceFollowRedirects(followRedirects); + int responseCode = con.getResponseCode(); con.getHeaderFields().forEach((key, value) -> { if (key != null) { - responseHeaders.put(key, value); + responseHeaders.put(key, value.get(0)); } }); @@ -262,30 +267,6 @@ public static T sendJsonPUTRequest(Main main, String requestID, String url, return sendJsonRequest(main, requestID, url, requestBody, connectionTimeoutMS, readTimeoutMS, version, "PUT"); } - public static T sendJsonPATCHRequest(Main main, String url, JsonElement requestBody) - throws IOException, HttpResponseException, InterruptedException { - - HttpClient client = null; - - String body = requestBody.toString(); - java.net.http.HttpRequest rawRequest = java.net.http.HttpRequest.newBuilder() - .uri(URI.create(url)) - .method("PATCH", java.net.http.HttpRequest.BodyPublishers.ofString(body)) - .build(); - client = HttpClient.newHttpClient(); - HttpResponse response = client.send(rawRequest, HttpResponse.BodyHandlers.ofString()); - - int responseCode = response.statusCode(); - - if (responseCode < STATUS_CODE_ERROR_THRESHOLD) { - if (!isJsonValid(response.body().toString())) { - return (T) response.body().toString(); - } - return (T) (new JsonParser().parse(response.body().toString())); - } - throw new HttpResponseException(responseCode, response.body().toString()); - } - public static T sendJsonDELETERequest(Main main, String requestID, String url, JsonElement requestBody, int connectionTimeoutMS, int readTimeoutMS, Integer version) throws IOException, HttpResponseException { @@ -293,4 +274,4 @@ public static T sendJsonDELETERequest(Main main, String requestID, String ur "DELETE"); } -} +} \ No newline at end of file diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index f0f928a6b..288e6593e 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -55,7 +55,6 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.multitenancy.sqlStorage.MultitenancySQLStorage; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; @@ -107,7 +106,6 @@ public class Start ActiveUsersSQLStorage, DashboardSQLStorage, AuthRecipeSQLStorage, OAuthSQLStorage { private static final Object appenderLock = new Object(); - private static final String APP_ID_KEY_NAME = "app_id"; private static final String ACCESS_TOKEN_SIGNING_KEY_NAME = "access_token_signing_key"; private static final String REFRESH_TOKEN_KEY_NAME = "refresh_token_key"; public static boolean isTesting = false; @@ -3011,7 +3009,7 @@ public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(A } @Override - public boolean doesClientIdExistForThisApp(AppIdentifier appIdentifier, String clientId) + public boolean doesClientIdExistForApp(AppIdentifier appIdentifier, String clientId) throws StorageQueryException { try { return OAuthQueries.isClientIdForAppId(this, clientId, appIdentifier); @@ -3021,19 +3019,11 @@ public boolean doesClientIdExistForThisApp(AppIdentifier appIdentifier, String c } @Override - public void addClientForApp(AppIdentifier appIdentifier, String clientId) - throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException { + public void addOrUpdateClientForApp(AppIdentifier appIdentifier, String clientId, boolean isClientCredentialsOnly) + throws StorageQueryException { try { - OAuthQueries.insertClientIdForAppId(this, clientId, appIdentifier); + OAuthQueries.insertClientIdForAppId(this, appIdentifier, clientId, isClientCredentialsOnly); } catch (SQLException e) { - - SQLiteConfig config = Config.getConfig(this); - String serverErrorMessage = e.getMessage(); - - if (isPrimaryKeyError(serverErrorMessage, config.getOAuthClientTable(), - new String[]{"app_id", "client_id"})) { - throw new OAuth2ClientAlreadyExistsForAppException(); - } throw new StorageQueryException(e); } } @@ -3046,4 +3036,91 @@ public boolean removeAppClientAssociation(AppIdentifier appIdentifier, String cl throw new StorageQueryException(e); } } + + @Override + public List listClientsForApp(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return OAuthQueries.listClientsForApp(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void revoke(AppIdentifier appIdentifier, String targetType, String targetValue, long exp) + throws StorageQueryException { + try { + OAuthQueries.revoke(this, appIdentifier, targetType, targetValue, exp); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + + } + + @Override + public boolean isRevoked(AppIdentifier appIdentifier, String[] targetTypes, String[] targetValues, long issuedAt) + throws StorageQueryException { + try { + return OAuthQueries.isRevoked(this, appIdentifier, targetTypes, targetValues, issuedAt); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void addM2MToken(AppIdentifier appIdentifier, String clientId, long iat, long exp) + throws StorageQueryException { + try { + OAuthQueries.addM2MToken(this, appIdentifier, clientId, iat, exp); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void cleanUpExpiredAndRevokedTokens(AppIdentifier appIdentifier) throws StorageQueryException { + try { + OAuthQueries.cleanUpExpiredAndRevokedTokens(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfClientCredentialsOnlyClientsForApp(AppIdentifier appIdentifier) + throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfClientsForApp(this, appIdentifier, true); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfClientsForApp(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfClientsForApp(this, appIdentifier, false); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfM2MTokensAlive(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfM2MTokensAlive(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfM2MTokensCreatedSince(AppIdentifier appIdentifier, long since) + throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfM2MTokensCreatedSince(this, appIdentifier, since); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } } diff --git a/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java b/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java index bc969dc6f..c14646456 100644 --- a/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java +++ b/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java @@ -165,5 +165,15 @@ public String getDashboardSessionsTable() { return "dashboard_user_sessions"; } - public String getOAuthClientTable(){ return "oauth_clients"; } + public String getOAuthClientsTable() { + return "oauth_clients"; + } + + public String getOAuthRevokeTable() { + return "oauth_revoke"; + } + + public String getOAuthM2MTokensTable() { + return "oauth_m2m_tokens"; + } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java index 665511467..13d4ee092 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java @@ -423,10 +423,27 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc update(start, TOTPQueries.getQueryToCreateUsedCodesExpiryTimeIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getOAuthClientTable())) { + if (!doesTableExists(start, Config.getConfig(start).getOAuthClientsTable())) { getInstance(main).addState(CREATING_NEW_TABLE, null); update(start, OAuthQueries.getQueryToCreateOAuthClientTable(start), NO_OP_SETTER); } + + if (!doesTableExists(start, Config.getConfig(start).getOAuthRevokeTable())) { + getInstance(main).addState(CREATING_NEW_TABLE, null); + update(start, OAuthQueries.getQueryToCreateOAuthRevokeTable(start), NO_OP_SETTER); + + // index + update(start, OAuthQueries.getQueryToCreateOAuthRevokeTimestampIndex(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, Config.getConfig(start).getOAuthM2MTokensTable())) { + getInstance(main).addState(CREATING_NEW_TABLE, null); + update(start, OAuthQueries.getQueryToCreateOAuthM2MTokensTable(start), NO_OP_SETTER); + + // index + update(start, OAuthQueries.getQueryToCreateOAuthM2MTokenIatIndex(start), NO_OP_SETTER); + update(start, OAuthQueries.getQueryToCreateOAuthM2MTokenExpIndex(start), NO_OP_SETTER); + } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java index f0441cbe2..5a27ee8d5 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java @@ -23,6 +23,8 @@ import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute; import static io.supertokens.inmemorydb.QueryExecutorTemplate.update; @@ -30,19 +32,69 @@ public class OAuthQueries { public static String getQueryToCreateOAuthClientTable(Start start) { - String oAuth2ClientTable = Config.getConfig(start).getOAuthClientTable(); + String oAuth2ClientTable = Config.getConfig(start).getOAuthClientsTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + oAuth2ClientTable + " (" + "app_id VARCHAR(64)," + "client_id VARCHAR(128) NOT NULL," + + "is_client_credentials_only BOOLEAN NOT NULL," + " PRIMARY KEY (app_id, client_id)," + " FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE);"; // @formatter:on } + public static String getQueryToCreateOAuthRevokeTable(Start start) { + String oAuth2RevokeTable = Config.getConfig(start).getOAuthRevokeTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + oAuth2RevokeTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "target_type VARCHAR(16) NOT NULL," + + "target_value VARCHAR(128) NOT NULL," + + "timestamp BIGINT NOT NULL, " + + "exp BIGINT NOT NULL," + + "PRIMARY KEY (app_id, target_type, target_value)," + + "FOREIGN KEY(app_id) " + + " REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + public static String getQueryToCreateOAuthRevokeTimestampIndex(Start start) { + String oAuth2RevokeTable = Config.getConfig(start).getOAuthRevokeTable(); + return "CREATE INDEX IF NOT EXISTS oauth_revoke_timestamp_index ON " + + oAuth2RevokeTable + "(timestamp DESC, app_id DESC);"; + } + + public static String getQueryToCreateOAuthM2MTokensTable(Start start) { + String oAuth2M2MTokensTable = Config.getConfig(start).getOAuthM2MTokensTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + oAuth2M2MTokensTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "client_id VARCHAR(128) NOT NULL," + + "iat BIGINT NOT NULL," + + "exp BIGINT NOT NULL," + + "PRIMARY KEY (app_id, client_id, iat)," + + "FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + public static String getQueryToCreateOAuthM2MTokenIatIndex(Start start) { + String oAuth2M2MTokensTable = Config.getConfig(start).getOAuthM2MTokensTable(); + return "CREATE INDEX IF NOT EXISTS oauth_m2m_token_iat_index ON " + + oAuth2M2MTokensTable + "(iat DESC, app_id DESC);"; + } + + public static String getQueryToCreateOAuthM2MTokenExpIndex(Start start) { + String oAuth2M2MTokensTable = Config.getConfig(start).getOAuthM2MTokensTable(); + return "CREATE INDEX IF NOT EXISTS oauth_m2m_token_exp_index ON " + + oAuth2M2MTokensTable + "(exp DESC, app_id DESC);"; + } + public static boolean isClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthClientTable() + + String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthClientsTable() + " WHERE client_id = ? AND app_id = ?"; return execute(start, QUERY, pst -> { @@ -51,19 +103,38 @@ public static boolean isClientIdForAppId(Start start, String clientId, AppIdenti }, ResultSet::next); } - public static void insertClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier) + public static List listClientsForApp(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String QUERY = "SELECT client_id FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE app_id = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + }, (result) -> { + List res = new ArrayList<>(); + while (result.next()) { + res.add(result.getString("client_id")); + } + return res; + }); + } + + public static void insertClientIdForAppId(Start start, AppIdentifier appIdentifier, String clientId, + boolean isClientCredentialsOnly) throws SQLException, StorageQueryException { - String INSERT = "INSERT INTO " + Config.getConfig(start).getOAuthClientTable() - + "(app_id, client_id) VALUES(?, ?)"; + String INSERT = "INSERT INTO " + Config.getConfig(start).getOAuthClientsTable() + + "(app_id, client_id, is_client_credentials_only) VALUES(?, ?, ?) " + + "ON CONFLICT (app_id, client_id) DO UPDATE SET is_client_credentials_only = ?"; update(start, INSERT, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, clientId); + pst.setBoolean(3, isClientCredentialsOnly); + pst.setBoolean(4, isClientCredentialsOnly); }); } public static boolean deleteClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthClientTable() + String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthClientsTable() + " WHERE app_id = ? AND client_id = ?"; int numberOfRow = update(start, DELETE, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -72,4 +143,146 @@ public static boolean deleteClientIdForAppId(Start start, String clientId, AppId return numberOfRow > 0; } + public static void revoke(Start start, AppIdentifier appIdentifier, String targetType, String targetValue, long exp) + throws SQLException, StorageQueryException { + String INSERT = "INSERT INTO " + Config.getConfig(start).getOAuthRevokeTable() + + "(app_id, target_type, target_value, timestamp, exp) VALUES (?, ?, ?, ?, ?) " + + "ON CONFLICT (app_id, target_type, target_value) DO UPDATE SET timestamp = ?, exp = ?"; + + long currentTime = System.currentTimeMillis() / 1000; + update(start, INSERT, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, targetType); + pst.setString(3, targetValue); + pst.setLong(4, currentTime); + pst.setLong(5, exp); + pst.setLong(6, currentTime); + pst.setLong(7, exp); + }); + } + + public static boolean isRevoked(Start start, AppIdentifier appIdentifier, String[] targetTypes, String[] targetValues, long issuedAt) + throws SQLException, StorageQueryException { + String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthRevokeTable() + + " WHERE app_id = ? AND timestamp > ? AND ("; + + for (int i = 0; i < targetTypes.length; i++) { + QUERY += "(target_type = ? AND target_value = ?)"; + + if (i < targetTypes.length - 1) { + QUERY += " OR "; + } + } + + QUERY += ")"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, issuedAt); + + int index = 3; + for (int i = 0; i < targetTypes.length; i++) { + pst.setString(index, targetTypes[i]); + index++; + pst.setString(index, targetValues[i]); + index++; + } + }, ResultSet::next); + } + + public static int countTotalNumberOfClientsForApp(Start start, AppIdentifier appIdentifier, + boolean filterByClientCredentialsOnly) throws SQLException, StorageQueryException { + if (filterByClientCredentialsOnly) { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE app_id = ? AND is_client_credentials_only = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setBoolean(2, true); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } else { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE app_id = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + } + + public static int countTotalNumberOfM2MTokensAlive(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " WHERE app_id = ? AND exp > ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, System.currentTimeMillis()/1000); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + + public static int countTotalNumberOfM2MTokensCreatedSince(Start start, AppIdentifier appIdentifier, long since) + throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " WHERE app_id = ? AND iat >= ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, since / 1000); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + + public static void addM2MToken(Start start, AppIdentifier appIdentifier, String clientId, long iat, long exp) + throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getOAuthM2MTokensTable() + + " (app_id, client_id, iat, exp) VALUES (?, ?, ?, ?)"; + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, clientId); + pst.setLong(3, iat); + pst.setLong(4, exp); + }); + } + + public static void cleanUpExpiredAndRevokedTokens(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { + { + // delete expired M2M tokens + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " WHERE app_id = ? AND exp < ?"; + + long timestamp = System.currentTimeMillis() / 1000 - 3600 * 24 * 31; // expired 31 days ago + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, timestamp); + }); + } + + { + // delete expired revoked tokens + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthRevokeTable() + + " WHERE app_id = ? AND exp < ?"; + + long timestamp = System.currentTimeMillis() / 1000 - 3600 * 24 * 31; // expired 31 days ago + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, timestamp); + }); + } + } } diff --git a/src/main/java/io/supertokens/oauth/HttpRequestForOry.java b/src/main/java/io/supertokens/oauth/HttpRequestForOry.java new file mode 100644 index 000000000..56e92bf55 --- /dev/null +++ b/src/main/java/io/supertokens/oauth/HttpRequestForOry.java @@ -0,0 +1,201 @@ +package io.supertokens.oauth; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.supertokens.oauth.exceptions.OAuthClientNotFoundException; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class HttpRequestForOry { + // 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 + // case of errors, etc. + // Left the original HttpRequest as is to avoid any issues with existing code. + + private static final int CONNECTION_TIMEOUT = 5000; + private static final int READ_TIMEOUT = 5000; + + public static Response doGet(String url, Map headers, Map queryParams) throws IOException { + if (queryParams == null) { + queryParams = new HashMap<>(); + } + URL obj = new URL(url + "?" + queryParams.entrySet().stream() + .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&"))); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setInstanceFollowRedirects(false); // Do not follow redirect + con.setRequestMethod("GET"); + con.setConnectTimeout(CONNECTION_TIMEOUT); + con.setReadTimeout(READ_TIMEOUT); + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + con.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + return getResponse(con); + } + + public static Response doFormPost(String url, Map headers, Map formFields) throws IOException, OAuthClientNotFoundException { + try { + URL obj = new URL(url); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setRequestMethod("POST"); + con.setConnectTimeout(CONNECTION_TIMEOUT); + con.setReadTimeout(READ_TIMEOUT); + con.setDoOutput(true); + con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + con.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + + try (DataOutputStream os = new DataOutputStream(con.getOutputStream())) { + os.writeBytes(formFields.entrySet().stream() + .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&"))); + } + return getResponse(con); + } catch (FileNotFoundException e) { + throw new OAuthClientNotFoundException(); + } + } + + public static Response doJsonPost(String url, Map headers, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { + try { + URL obj = new URL(url); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setRequestMethod("POST"); + con.setConnectTimeout(CONNECTION_TIMEOUT); + con.setReadTimeout(READ_TIMEOUT); + con.setDoOutput(true); + con.setRequestProperty("Content-Type", "application/json"); + + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + con.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + + try (DataOutputStream os = new DataOutputStream(con.getOutputStream())) { + os.writeBytes(jsonInput.toString()); + } + return getResponse(con); + } catch (FileNotFoundException e) { + throw new OAuthClientNotFoundException(); + } + } + + public static Response doJsonPut(String url, Map queryParams, Map headers, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { + try { + if (queryParams == null) { + queryParams = new HashMap<>(); + } + URL obj = new URL(url + "?" + queryParams.entrySet().stream() + .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&"))); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setRequestMethod("PUT"); + con.setConnectTimeout(CONNECTION_TIMEOUT); + con.setReadTimeout(READ_TIMEOUT); + con.setDoOutput(true); + con.setRequestProperty("Content-Type", "application/json"); + + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + con.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + + try (DataOutputStream os = new DataOutputStream(con.getOutputStream())) { + os.writeBytes(jsonInput.toString()); + } + return getResponse(con); + } catch (FileNotFoundException e) { + throw new OAuthClientNotFoundException(); + } + } + + public static Response doJsonDelete(String url, Map headers, Map queryParams, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { + try { + if (queryParams == null) { + queryParams = new HashMap<>(); + } + + URL obj = new URL(url + "?" + queryParams.entrySet().stream() + .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&"))); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setRequestMethod("DELETE"); + con.setConnectTimeout(CONNECTION_TIMEOUT); + con.setReadTimeout(READ_TIMEOUT); + con.setDoOutput(true); + con.setRequestProperty("Content-Type", "application/json"); + + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + con.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + + if (jsonInput != null) { + try (DataOutputStream os = new DataOutputStream(con.getOutputStream())) { + os.writeBytes(jsonInput.toString()); + } + } + + return getResponse(con); + } catch (FileNotFoundException e) { + throw new OAuthClientNotFoundException(); + } + } + + private static Response getResponse(HttpURLConnection con) throws IOException { + int responseCode = con.getResponseCode(); + InputStream inputStream; + if (con.getErrorStream() != null) { + inputStream = con.getErrorStream(); + } else { + inputStream = con.getInputStream(); + } + BufferedReader in = new BufferedReader(new InputStreamReader(inputStream)); + + String inputLine; + StringBuilder response = new StringBuilder(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + JsonElement jsonResponse = null; + if (con.getContentType() != null && con.getContentType().contains("application/json")) { + Gson gson = new Gson(); + jsonResponse = gson.fromJson(response.toString(), JsonElement.class); + } + return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); + } + + public static class Response { + public int statusCode; + public String rawResponse; + public JsonElement jsonResponse; + public Map> headers; + + public Response(int statusCode, String rawResponse, JsonElement jsonResponse, Map> headers) { + this.statusCode = statusCode; + this.rawResponse = rawResponse; + this.jsonResponse = jsonResponse; + this.headers = headers; + } + } +} diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 6b7ef3488..0c73d6a33 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -16,295 +16,580 @@ package io.supertokens.oauth; +import com.auth0.jwt.exceptions.JWTCreationException; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; + import io.supertokens.Main; import io.supertokens.config.Config; -import io.supertokens.httpRequest.HttpRequest; -import io.supertokens.httpRequest.HttpResponseException; +import io.supertokens.exceptions.TryRefreshTokenException; +import io.supertokens.featureflag.EE_FEATURES; +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.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.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.oauth.OAuthStorage; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; +import io.supertokens.session.jwt.JWT.JWTException; import io.supertokens.utils.Utils; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; import java.util.*; +import java.util.Map.Entry; public class OAuth { + private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) + throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException { + EE_FEATURES[] features = FeatureFlag.getInstance(main, appIdentifier).getEnabledFeatures(); + for (EE_FEATURES f : features) { + if (f == EE_FEATURES.OAUTH) { + return; + } + } + + throw new FeatureNotEnabledException( + "OAuth feature is not enabled. Please subscribe to a SuperTokens core license key to enable this " + + "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 { + checkForOauthFeature(appIdentifier, main); + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + + if (camelToSnakeCaseConversion) { + queryParams = convertCamelToSnakeCase(queryParams); + } + + if (clientIdToCheck != null) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { + throw new OAuthClientNotFoundException(); + } + } + + // Request transformations + headers = Transformations.transformRequestHeadersForHydra(headers); + + String baseURL; + if (proxyToAdmin) { + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); + } else { + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); + } + String fullUrl = baseURL + path; + + HttpRequestForOry.Response response = HttpRequestForOry.doGet(fullUrl, headers, queryParams); + + // Response transformations + response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); + response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); + + checkNonSuccessResponse(response); - private static final String LOCATION_HEADER_NAME = "Location"; - private static final String COOKIES_HEADER_NAME = "Set-Cookie"; - private static final String ERROR_LITERAL = "error="; - private static final String ERROR_DESCRIPTION_LITERAL = "error_description="; + if (camelToSnakeCaseConversion) { + response.jsonResponse = convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } - private static final String HYDRA_AUTH_ENDPOINT = "/oauth2/auth"; - private static final String HYDRA_CLIENTS_ENDPOINT = "/admin/clients"; - public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject paramsFromSdk) - throws InvalidConfigException, HttpResponseException, IOException, OAuthAuthException, StorageQueryException, - TenantOrAppNotFoundException { + 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 { + checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - String redirectTo = null; - List cookies = null; + if (camelToSnakeCaseConversion) { + formFields = OAuth.convertCamelToSnakeCase(formFields); + } - String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); - String hydraInternalAddress = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOauthProviderUrlConfiguredInHydra(); - String hydraBaseUrlForConsentAndLogin = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOauthProviderConsentLoginBaseUrl(); + if (clientIdToCheck != null) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { + throw new OAuthClientNotFoundException(); + } + } - String clientId = paramsFromSdk.get("clientId").getAsString(); + // Request transformations + formFields = Transformations.transformFormFieldsForHydra(formFields); + headers = Transformations.transformRequestHeadersForHydra(headers); - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthAuthException("invalid_client", "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The requested OAuth 2.0 Client does not exist."); + String baseURL; + if (proxyToAdmin) { + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); } else { - // we query hydra - Map queryParamsForHydra = constructHydraRequestParamsForAuthorizationGETAPICall(paramsFromSdk); - Map> responseHeaders = new HashMap<>(); - - HttpRequest.sendGETRequestWithResponseHeaders(main, "", publicOAuthProviderServiceUrl + HYDRA_AUTH_ENDPOINT, queryParamsForHydra, 10000, 10000, null, responseHeaders, false); - - if(!responseHeaders.isEmpty() && responseHeaders.containsKey(LOCATION_HEADER_NAME)) { - String locationHeaderValue = responseHeaders.get(LOCATION_HEADER_NAME).get(0); - if(Utils.containsUrl(locationHeaderValue, hydraInternalAddress, true)){ - String error = getValueOfQueryParam(locationHeaderValue, ERROR_LITERAL); - String errorDescription = getValueOfQueryParam(locationHeaderValue, ERROR_DESCRIPTION_LITERAL); - throw new OAuthAuthException(error, errorDescription); - } + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); + } + String fullUrl = baseURL + path; - if(Utils.containsUrl(locationHeaderValue, hydraBaseUrlForConsentAndLogin, true)){ - redirectTo = locationHeaderValue.replace(hydraBaseUrlForConsentAndLogin, "{apiDomain}"); - } else { - redirectTo = locationHeaderValue; - } - } else { - throw new RuntimeException("Unexpected answer from Oauth Provider"); - } - if(responseHeaders.containsKey(COOKIES_HEADER_NAME)){ - cookies = responseHeaders.get(COOKIES_HEADER_NAME); - } + HttpRequestForOry.Response response = HttpRequestForOry.doFormPost(fullUrl, headers, formFields); + + // Response transformations + response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); + response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); + + checkNonSuccessResponse(response); + + if (camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); } - return new OAuthAuthResponse(redirectTo, cookies); + return response; } - //This more or less acts as a pass-through for the sdks, apart from camelCase <-> snake_case key transformation and setting a few default values - public static JsonObject registerOAuthClient(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject paramsFromSdk) - throws TenantOrAppNotFoundException, InvalidConfigException, IOException, - OAuthAPIInvalidInputException, - NoSuchAlgorithmException, StorageQueryException { - + 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 { + checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); - - byte[] idBaseBytes = new byte[48]; - while(true){ - new SecureRandom().nextBytes(idBaseBytes); - String clientId = "supertokens_" + Utils.hashSHA256Base64UrlSafe(idBaseBytes); - try { + if (camelToSnakeCaseConversion) { + jsonInput = convertCamelToSnakeCase(jsonInput); + } - JsonObject hydraRequestBody = constructHydraRequestParamsForRegisterClientPOST(paramsFromSdk, clientId); - JsonObject hydraResponse = HttpRequest.sendJsonPOSTRequest(main, "", adminOAuthProviderServiceUrl + HYDRA_CLIENTS_ENDPOINT, hydraRequestBody, 10000, 10000, null); - - oauthStorage.addClientForApp(appIdentifier, clientId); - - return formatResponseForSDK(hydraResponse); //sdk expects everything from hydra in camelCase - } catch (HttpResponseException e) { - try { - if (e.statusCode == 409){ - //no-op - //client with id already exists, silently retry with different Id - } else { - //other error from hydra, like invalid content in json. Throw exception - throw createCustomExceptionFromHttpResponseException( - e, OAuthAPIInvalidInputException.class); - } - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | - IllegalAccessException ex) { - throw new RuntimeException(ex); - } - } catch (OAuth2ClientAlreadyExistsForAppException e) { - //in theory, this is unreachable. We are registering new clients here, so this should not happen. - throw new RuntimeException(e); + if (clientIdToCheck != null) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { + throw new OAuthClientNotFoundException(); } } + + // Request transformations + jsonInput = Transformations.transformJsonForHydra(jsonInput); + headers = Transformations.transformRequestHeadersForHydra(headers); + + String baseURL; + if (proxyToAdmin) { + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); + } else { + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); + } + String fullUrl = baseURL + path; + + HttpRequestForOry.Response response = HttpRequestForOry.doJsonPost(fullUrl, headers, jsonInput); + + // Response transformations + response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); + response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); + + checkNonSuccessResponse(response); + + if (camelToSnakeCaseConversion) { + response.jsonResponse = convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + + return response; } - public static JsonObject loadOAuthClient(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) - throws TenantOrAppNotFoundException, InvalidConfigException, StorageQueryException, - IOException, OAuthClientNotFoundException { + 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 { + checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); + if (camelToSnakeCaseConversion) { + queryParams = convertCamelToSnakeCase(queryParams); + jsonInput = convertCamelToSnakeCase(jsonInput); + } - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException("Unable to locate the resource", ""); - } else { - try { - JsonObject hydraResponse = HttpRequest.sendGETRequest(main, "", adminOAuthProviderServiceUrl + HYDRA_CLIENTS_ENDPOINT + "/" + clientId, null, 10000, 10000, null); - return formatResponseForSDK(hydraResponse); - } catch (HttpResponseException e) { - try { - throw createCustomExceptionFromHttpResponseException(e, OAuthClientNotFoundException.class); - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | - IllegalAccessException ex) { - throw new RuntimeException(ex); - } + if (clientIdToCheck != null) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { + throw new OAuthClientNotFoundException(); } } + + // Request transformations + jsonInput = Transformations.transformJsonForHydra(jsonInput); + headers = Transformations.transformRequestHeadersForHydra(headers); + + String baseURL; + if (proxyToAdmin) { + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); + } else { + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); + } + String fullUrl = baseURL + path; + + HttpRequestForOry.Response response = HttpRequestForOry.doJsonPut(fullUrl, queryParams, headers, jsonInput); + + // Response transformations + response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); + response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); + + checkNonSuccessResponse(response); + + if (camelToSnakeCaseConversion) { + response.jsonResponse = convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + + return response; } - public static void deleteOAuthClient(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) - throws TenantOrAppNotFoundException, InvalidConfigException, StorageQueryException, - IOException, OAuthClientNotFoundException { + public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); + if (camelToSnakeCaseConversion) { + jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); + } + + if (clientIdToCheck != null) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { + throw new OAuthClientNotFoundException(); + } + } + + // Request transformations + jsonInput = Transformations.transformJsonForHydra(jsonInput); + headers = Transformations.transformRequestHeadersForHydra(headers); - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException("Unable to locate the resource", ""); + String baseURL; + if (proxyToAdmin) { + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); } else { - try { - oauthStorage.removeAppClientAssociation(appIdentifier, clientId); - HttpRequest.sendJsonDELETERequest(main, "", adminOAuthProviderServiceUrl + HYDRA_CLIENTS_ENDPOINT + "/" + clientId, null, 10000, 10000, null); - } catch (HttpResponseException e) { - try { - throw createCustomExceptionFromHttpResponseException(e, OAuthClientNotFoundException.class); - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | - IllegalAccessException ex) { - throw new RuntimeException(ex); - } + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); + } + String fullUrl = baseURL + path; + + HttpRequestForOry.Response response = HttpRequestForOry.doJsonDelete(fullUrl, queryParams, headers, jsonInput); + + // Response transformations + response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); + response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); + + checkNonSuccessResponse(response); + + if (camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + + return response; + } + + private static void checkNonSuccessResponse(HttpRequestForOry.Response response) throws OAuthAPIException, OAuthClientNotFoundException { + if (response.statusCode == 404) { + throw new OAuthClientNotFoundException(); + } + if (response.statusCode >= 400) { + String error = response.jsonResponse.getAsJsonObject().get("error").getAsString(); + String errorDescription = null; + if (response.jsonResponse.getAsJsonObject().has("error_description")) { + errorDescription = response.jsonResponse.getAsJsonObject().get("error_description").getAsString(); } + throw new OAuthAPIException(error, errorDescription, response.statusCode); } } - public static JsonObject updateOauthClient(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject paramsFromSdk) - throws TenantOrAppNotFoundException, InvalidConfigException, StorageQueryException, - InvocationTargetException, NoSuchMethodException, InstantiationException, - IllegalAccessException, OAuthClientNotFoundException, OAuthAPIInvalidInputException, - OAuthClientUpdateException { + public static String transformTokensInAuthRedirect(Main main, AppIdentifier appIdentifier, Storage storage, String url, String iss, JsonObject accessTokenUpdate, JsonObject idTokenUpdate, boolean useDynamicKey) { + if (url.indexOf('#') == -1) { + return url; + } - OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); + try { + // Extract the part after '#' + String fragment = url.substring(url.indexOf('#') + 1); + + // Parse the fragment as query parameters + // Create a JsonObject from the params + JsonObject jsonBody = new JsonObject(); + for (String param : fragment.split("&")) { + String[] keyValue = param.split("=", 2); + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = java.net.URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8.toString()); + jsonBody.addProperty(key, value); + } + } - String clientId = paramsFromSdk.get("clientId").getAsString(); + // Transform the tokens + JsonObject transformedJson = transformTokens(main, appIdentifier, storage, jsonBody, iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException("Unable to locate the resource", ""); - } else { - JsonArray hydraInput = translateIncomingDataToHydraUpdateFormat(paramsFromSdk); - try { - JsonObject updatedClient = HttpRequest.sendJsonPATCHRequest(main, adminOAuthProviderServiceUrl + HYDRA_CLIENTS_ENDPOINT+ "/" + clientId, hydraInput); - return formatResponseForSDK(updatedClient); - } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); - } catch (HttpResponseException e) { - int responseStatusCode = e.statusCode; - switch (responseStatusCode){ - case 400 -> throw createCustomExceptionFromHttpResponseException(e, OAuthAPIInvalidInputException.class); - case 404 -> throw createCustomExceptionFromHttpResponseException(e, OAuthClientNotFoundException.class); - case 500 -> throw createCustomExceptionFromHttpResponseException(e, OAuthClientUpdateException.class); // hydra is not so helpful with the error messages at this endpoint.. - default -> throw new RuntimeException(e); + // Reconstruct the query params + StringBuilder newFragment = new StringBuilder(); + for (Map.Entry entry : transformedJson.entrySet()) { + if (newFragment.length() > 0) { + newFragment.append("&"); } + String encodedValue = java.net.URLEncoder.encode(entry.getValue().getAsString(), StandardCharsets.UTF_8.toString()); + newFragment.append(entry.getKey()).append("=").append(encodedValue); } + + // Reconstruct the URL + String baseUrl = url.substring(0, url.indexOf('#')); + return baseUrl + "#" + newFragment.toString(); + } catch (Exception e) { + // If any exception occurs, return the original URL + return url; } } - private static JsonArray translateIncomingDataToHydraUpdateFormat(JsonObject input){ - JsonArray hydraPatchFormat = new JsonArray(); - for (Map.Entry changeIt : input.entrySet()) { - if (changeIt.getKey().equals("clientId")) { - continue; // we are not updating clientIds! + public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject jsonBody, String iss, JsonObject accessTokenUpdate, JsonObject idTokenUpdate, boolean useDynamicKey) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { + String atHash = null; + + if (jsonBody.has("refresh_token")) { + String refreshToken = jsonBody.get("refresh_token").getAsString(); + refreshToken = refreshToken.replace("ory_rt_", "st_rt_"); + jsonBody.addProperty("refresh_token", refreshToken); + } + + if (jsonBody.has("access_token")) { + String accessToken = jsonBody.get("access_token").getAsString(); + accessToken = OAuthToken.reSignToken(appIdentifier, main, accessToken, iss, accessTokenUpdate, null, OAuthToken.TokenType.ACCESS_TOKEN, useDynamicKey, 0); + jsonBody.addProperty("access_token", accessToken); + + // Compute at_hash as per OAuth 2.0 standard + // 1. Take the access token + // 2. Hash it with SHA-256 + // 3. Take the left-most half of the hash + // 4. Base64url encode it + byte[] accessTokenBytes = accessToken.getBytes(StandardCharsets.UTF_8); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(accessTokenBytes); + byte[] halfHash = Arrays.copyOf(hash, hash.length / 2); + atHash = Base64.getUrlEncoder().withoutPadding().encodeToString(halfHash); + } + + if (jsonBody.has("id_token")) { + String idToken = jsonBody.get("id_token").getAsString(); + idToken = OAuthToken.reSignToken(appIdentifier, main, idToken, iss, idTokenUpdate, atHash, OAuthToken.TokenType.ID_TOKEN, useDynamicKey, 0); + jsonBody.addProperty("id_token", idToken); + } + + return jsonBody; + } + + public static void addOrUpdateClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId, boolean isClientCredentialsOnly) throws StorageQueryException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.addOrUpdateClientForApp(appIdentifier, clientId, isClientCredentialsOnly); + } + + public static void removeClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.removeAppClientAssociation(appIdentifier, clientId); + } + + public static List listClientIds(Main main, AppIdentifier appIdentifier, Storage storage) throws StorageQueryException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + return oauthStorage.listClientsForApp(appIdentifier); + } + + private static Map convertCamelToSnakeCase(Map queryParams) { + Map result = new HashMap<>(); + if (queryParams != null) { + for (Map.Entry entry : queryParams.entrySet()) { + result.put(Utils.camelCaseToSnakeCase(entry.getKey()), entry.getValue()); } - hydraPatchFormat.add(translateToHydraPatch(changeIt.getKey(),changeIt.getValue())); } + return result; + } - return hydraPatchFormat; + public static JsonObject convertCamelToSnakeCase(JsonObject queryParams) { + JsonObject result = new JsonObject(); + for (Map.Entry entry : queryParams.entrySet()) { + result.add(Utils.camelCaseToSnakeCase(entry.getKey()), entry.getValue()); + } + return result; } - private static JsonObject translateToHydraPatch(String elementName, JsonElement newValue){ - JsonObject patchFormat = new JsonObject(); - String hydraElementName = Utils.camelCaseToSnakeCase(elementName); - patchFormat.addProperty("from", "/" + hydraElementName); - patchFormat.addProperty("path", "/" + hydraElementName); - patchFormat.addProperty("op", "replace"); // What was sent by the sdk should be handled as a complete new value for the property - patchFormat.add("value", newValue); + private static JsonElement convertSnakeCaseToCamelCaseRecursively(JsonElement jsonResponse) { + if (jsonResponse == null) { + return null; + } + + if (jsonResponse.isJsonObject()) { + JsonObject result = new JsonObject(); + for (Entry entry: jsonResponse.getAsJsonObject().entrySet()) { + String key = entry.getKey(); + JsonElement value = entry.getValue(); + if (value.isJsonObject()) { + value = convertSnakeCaseToCamelCaseRecursively(value.getAsJsonObject()); + } + result.add(Utils.snakeCaseToCamelCase(key), value); + } + return result; + } else if (jsonResponse.isJsonArray()) { + JsonArray result = new JsonArray(); + for (JsonElement element : jsonResponse.getAsJsonArray()) { + result.add(convertSnakeCaseToCamelCaseRecursively(element)); + } + return result; + } + return jsonResponse; - return patchFormat; } - private static T createCustomExceptionFromHttpResponseException(HttpResponseException exception, Class customExceptionClass) - throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { - String errorMessage = exception.rawMessage; - JsonObject errorResponse = (JsonObject) new JsonParser().parse(errorMessage); - String error = errorResponse.get("error").getAsString(); - String errorDescription = errorResponse.get("error_description").getAsString(); - return customExceptionClass.getDeclaredConstructor(String.class, String.class).newInstance(error, errorDescription); + public static void verifyAndUpdateIntrospectRefreshTokenPayload(Main main, AppIdentifier appIdentifier, + Storage storage, JsonObject payload, String refreshToken) throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException { + + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + + if (!payload.get("active").getAsBoolean()) { + return; // refresh token is not active + } + + Transformations.transformExt(payload); + payload.remove("ext"); + + boolean isValid = !isTokenRevokedBasedOnPayload(oauthStorage, appIdentifier, payload); + + if (!isValid) { + payload.entrySet().clear(); + payload.addProperty("active", false); + + // // ideally we want to revoke the refresh token in hydra, but we can't since we don't have the client secret here + // refreshToken = refreshToken.replace("st_rt_", "ory_rt_"); + // Map formFields = new HashMap<>(); + // formFields.put("token", refreshToken); + + // try { + // doOAuthProxyFormPOST( + // main, appIdentifier, oauthStorage, + // clientId, // clientIdToCheck + // "/oauth2/revoke", // path + // false, // proxyToAdmin + // false, // camelToSnakeCaseConversion + // formFields, + // new HashMap<>()); + // } catch (OAuthAPIException | OAuthClientNotFoundException e) { + // // ignore + // } + } } - private static JsonObject constructHydraRequestParamsForRegisterClientPOST(JsonObject paramsFromSdk, String generatedClientId){ - JsonObject requestBody = new JsonObject(); + private static boolean isTokenRevokedBasedOnPayload(OAuthStorage oauthStorage, AppIdentifier appIdentifier, JsonObject payload) throws StorageQueryException { + long issuedAt = payload.get("iat").getAsLong(); + List targetTypes = new ArrayList<>(); + List targetValues = new ArrayList<>(); + + targetTypes.add("client_id"); + targetValues.add(payload.get("client_id").getAsString()); - //translating camelCase keys to snakeCase keys - for (Map.Entry jsonEntry : paramsFromSdk.entrySet()){ - requestBody.add(Utils.camelCaseToSnakeCase(jsonEntry.getKey()), jsonEntry.getValue()); + if (payload.has("jti")) { + targetTypes.add("jti"); + targetValues.add(payload.get("jti").getAsString()); } - //add client_id - requestBody.addProperty("client_id", generatedClientId); + if (payload.has("gid")) { + targetTypes.add("gid"); + targetValues.add(payload.get("gid").getAsString()); + } - //setting other non-changing defaults - requestBody.addProperty("access_token_strategy", "jwt"); - requestBody.addProperty("skip_consent", true); - requestBody.addProperty("subject_type", "public"); + if (payload.has("sessionHandle")) { + targetTypes.add("session_handle"); + targetValues.add(payload.get("sessionHandle").getAsString()); + } - return requestBody; + return oauthStorage.isRevoked(appIdentifier, targetTypes.toArray(new String[0]), targetValues.toArray(new String[0]), issuedAt); } - private static JsonObject formatResponseForSDK(JsonObject response) { - JsonObject formattedResponse = new JsonObject(); + public static JsonObject introspectAccessToken(Main main, AppIdentifier appIdentifier, Storage storage, + String token) throws StorageQueryException, StorageTransactionLogicException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException { + try { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + JsonObject payload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, token); + + if (payload.has("stt") && payload.get("stt").getAsInt() == OAuthToken.TokenType.ACCESS_TOKEN.getValue()) { - //translating snake_case keys to camelCase keys - for (Map.Entry jsonEntry : response.entrySet()){ - formattedResponse.add(Utils.snakeCaseToCamelCase(jsonEntry.getKey()), jsonEntry.getValue()); + boolean isValid = !isTokenRevokedBasedOnPayload(oauthStorage, appIdentifier, payload); + + if (isValid) { + payload.addProperty("active", true); + payload.addProperty("token_type", "Bearer"); + payload.addProperty("token_use", "access_token"); + + return payload; + } + } + // else fallback to active: false + + } catch (TryRefreshTokenException e) { + // fallback to active: false } - return formattedResponse; + JsonObject result = new JsonObject(); + result.addProperty("active", false); + return result; + } + + public static void revokeTokensForClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException { + long exp = System.currentTimeMillis() / 1000 + 3600 * 24 * 183; // 6 month from now + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.revoke(appIdentifier, "client_id", clientId, exp); } - private static Map constructHydraRequestParamsForAuthorizationGETAPICall(JsonObject inputFromSdk) { - Map queryParamsForHydra = new HashMap<>(); - for(Map.Entry jsonElement : inputFromSdk.entrySet()){ - queryParamsForHydra.put(Utils.camelCaseToSnakeCase(jsonElement.getKey()), jsonElement.getValue().getAsString()); + public static void revokeRefreshToken(Main main, AppIdentifier appIdentifier, Storage storage, String gid, long exp) throws StorageQueryException, NoSuchAlgorithmException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.revoke(appIdentifier, "gid", gid, exp); + } + + public static void revokeAccessToken(Main main, AppIdentifier appIdentifier, + Storage storage, String token) throws StorageQueryException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + try { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + JsonObject payload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, token); + + long exp = payload.get("exp").getAsLong(); + + if (payload.has("stt") && payload.get("stt").getAsInt() == OAuthToken.TokenType.ACCESS_TOKEN.getValue()) { + String jti = payload.get("jti").getAsString(); + oauthStorage.revoke(appIdentifier, "jti", jti, exp); + } + + } catch (TryRefreshTokenException e) { + // the token is already invalid or revoked, so ignore } - return queryParamsForHydra; } - private static String getValueOfQueryParam(String url, String queryParam){ - String valueOfQueryParam = ""; - if(!queryParam.endsWith("=")){ - queryParam = queryParam + "="; + public static void revokeSessionHandle(Main main, AppIdentifier appIdentifier, Storage storage, + String sessionHandle) throws StorageQueryException { + long exp = System.currentTimeMillis() / 1000 + 3600 * 24 * 183; // 6 month from now + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.revoke(appIdentifier, "session_handle", sessionHandle, exp); + } + + public static void verifyIdTokenHintClientIdAndUpdateQueryParamsForLogout(Main main, AppIdentifier appIdentifier, Storage storage, + Map queryParams) throws StorageQueryException, OAuthAPIException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + + String idTokenHint = queryParams.get("idTokenHint"); + String clientId = queryParams.get("clientId"); + + JsonObject idTokenPayload = null; + if (idTokenHint != null) { + queryParams.remove("idTokenHint"); + + try { + idTokenPayload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, idTokenHint); + } catch (TryRefreshTokenException e) { + // invalid id token + throw new OAuthAPIException("invalid_request", "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.", 400); + } } - int startIndex = url.indexOf(queryParam) + queryParam.length(); // start after the '=' sign - int endIndex = url.indexOf("&", startIndex); - if (endIndex == -1){ - endIndex = url.length(); + + if (idTokenPayload != null) { + if (!idTokenPayload.has("stt") || idTokenPayload.get("stt").getAsInt() != OAuthToken.TokenType.ID_TOKEN.getValue()) { + // Invalid id token + throw new OAuthAPIException("invalid_request", "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.", 400); + } + + String clientIdInIdTokenPayload = idTokenPayload.get("aud").getAsString(); + + if (clientId != null) { + if (!clientId.equals(clientIdInIdTokenPayload)) { + throw new OAuthAPIException("invalid_request", "The client_id in the id_token_hint does not match the client_id in the request.", 400); + } + } + + queryParams.put("clientId", clientIdInIdTokenPayload); } - valueOfQueryParam = url.substring(startIndex, endIndex); // substring the url from the '=' to the next '&' or to the end of the url if there are no more &s - return URLDecoder.decode(valueOfQueryParam, StandardCharsets.UTF_8); } -;} + + public static void addM2MToken(Main main, AppIdentifier appIdentifier, Storage storage, String accessToken) throws StorageQueryException, TenantOrAppNotFoundException, TryRefreshTokenException, UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + JsonObject payload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, accessToken); + oauthStorage.addM2MToken(appIdentifier, payload.get("client_id").getAsString(), payload.get("iat").getAsLong(), payload.get("exp").getAsLong()); + } +} diff --git a/src/main/java/io/supertokens/oauth/OAuthToken.java b/src/main/java/io/supertokens/oauth/OAuthToken.java new file mode 100644 index 000000000..0d92b0b96 --- /dev/null +++ b/src/main/java/io/supertokens/oauth/OAuthToken.java @@ -0,0 +1,148 @@ +package io.supertokens.oauth; + +import com.auth0.jwt.exceptions.JWTCreationException; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.exceptions.TryRefreshTokenException; +import io.supertokens.jwt.JWTSigningFunctions; +import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.jwt.JWTAsymmetricSigningKeyInfo; +import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.session.jwt.JWT; +import io.supertokens.session.jwt.JWT.JWTException; +import io.supertokens.signingkeys.JWTSigningKey; +import io.supertokens.signingkeys.SigningKeys; +import io.supertokens.utils.Utils; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.security.InvalidKeyException; +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; + +public class OAuthToken { + public enum TokenType { + ACCESS_TOKEN(1), + ID_TOKEN(2); + + private final int value; + + TokenType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + private static Set NON_OVERRIDABLE_TOKEN_PROPS = Set.of( + "kid", "typ", "alg", "aud", + "iss", "iat", "exp", "nbf", "jti", "ext", + "sid", "rat", "at_hash", "gid", + "client_id", "scp", "sub", "stt" + ); + + public static JsonObject getPayloadFromJWTToken(AppIdentifier appIdentifier, + @Nonnull Main main, @Nonnull String token) + throws TenantOrAppNotFoundException, TryRefreshTokenException, StorageQueryException, + UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + List keyInfoList = SigningKeys.getInstance(appIdentifier, main).getAllKeys(); + Exception error = null; + JWT.JWTInfo jwtInfo = null; + JWT.JWTPreParseInfo preParseJWTInfo = null; + try { + preParseJWTInfo = JWT.preParseJWTInfo(token); + } catch (JWTException e) { + // This basically should never happen, but it means, that the token structure is + // wrong, can't verify + throw new TryRefreshTokenException(e); + } + + for (JWTSigningKeyInfo keyInfo : keyInfoList) { + try { + jwtInfo = JWT.verifyJWTAndGetPayload(preParseJWTInfo, + ((JWTAsymmetricSigningKeyInfo) keyInfo).publicKey); + error = null; + break; + } catch (NoSuchAlgorithmException e) { + // This basically should never happen, but it means, that can't verify any + // tokens, no need to retry + throw new TryRefreshTokenException(e); + } catch (KeyException | JWTException e) { + error = e; + } + } + + if (jwtInfo == null) { + throw new TryRefreshTokenException(error); + } + + if (jwtInfo.payload.get("exp").getAsLong() * 1000 < System.currentTimeMillis()) { + throw new TryRefreshTokenException("Access token expired"); + } + + return jwtInfo.payload; + } + + public static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, JsonObject payloadUpdate, String atHash, TokenType tokenType, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, + JWTCreationException { + JsonObject payload = JWT.getPayloadWithoutVerifying(token).payload; + + payload.addProperty("iss", iss); + payload.addProperty("stt", tokenType.getValue()); + if (atHash != null) { + payload.addProperty("at_hash", atHash); + } + + if (tokenType == TokenType.ACCESS_TOKEN) { + // we need to move rsub, tId and sessionHandle from ext to root + Transformations.transformExt(payload); + } + + // This should only happen in the authorization code flow during the token exchange. (enforced on the api level) + // Other flows (including later calls using the refresh token) will have the payloadUpdate defined. + if (payloadUpdate == null) { + if (tokenType == TokenType.ACCESS_TOKEN) { + if (payload.has("ext") && payload.get("ext").isJsonObject()) { + payloadUpdate = payload.getAsJsonObject("ext").getAsJsonObject("initialPayload"); + payload.remove("ext"); + } + } else { + payloadUpdate = payload.getAsJsonObject("initialPayload"); + payload.remove("initialPayload"); + } + } + + if (payloadUpdate != null) { + for (Map.Entry entry : payloadUpdate.entrySet()) { + if (!NON_OVERRIDABLE_TOKEN_PROPS.contains(entry.getKey())) { + payload.add(entry.getKey(), entry.getValue()); + } + } + } + + JWTSigningKeyInfo keyToUse; + if (useDynamicSigningKey) { + keyToUse = Utils.getJWTSigningKeyInfoFromKeyInfo( + SigningKeys.getInstance(appIdentifier, main).getLatestIssuedDynamicKey()); + } else { + keyToUse = SigningKeys.getInstance(appIdentifier, main) + .getStaticKeyForAlgorithm(JWTSigningKey.SupportedAlgorithms.RS256); + } + + token = JWTSigningFunctions.createJWTToken(JWTSigningKey.SupportedAlgorithms.RS256, new HashMap<>(), + payload, null, payload.get("exp").getAsLong(), payload.get("iat").getAsLong(), keyToUse); + return token; + } +} diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java new file mode 100644 index 000000000..aa842470b --- /dev/null +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -0,0 +1,266 @@ +package io.supertokens.oauth; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.config.Config; +import io.supertokens.oauth.exceptions.OAuthAPIException; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.utils.Utils; + +public class Transformations { + private static Set EXT_PROPS = Set.of("iss", "rsub", "tId", "sessionHandle", "gid"); + + private static Set CLIENT_PROPS = Set.of( + "clientId", + "clientSecret", + "clientName", + "scope", + "redirectUris", + "postLogoutRedirectUris", + "authorizationCodeGrantAccessTokenLifespan", + "authorizationCodeGrantIdTokenLifespan", + "authorizationCodeGrantRefreshTokenLifespan", + "clientCredentialsGrantAccessTokenLifespan", + "implicitGrantAccessTokenLifespan", + "implicitGrantIdTokenLifespan", + "refreshTokenGrantAccessTokenLifespan", + "refreshTokenGrantIdTokenLifespan", + "refreshTokenGrantRefreshTokenLifespan", + "tokenEndpointAuthMethod", + "clientUri", + "allowedCorsOrigins", + "audience", + "grantTypes", + "responseTypes", + "logoUri", + "policyUri", + "tosUri", + "createdAt", + "updatedAt", + "metadata" + ); + + public static Map transformRequestHeadersForHydra(Map requestHeaders) { + if (requestHeaders == null) { + return requestHeaders; + } + + if (requestHeaders.containsKey("Cookie")) { + String cookieValue = requestHeaders.get("Cookie"); + cookieValue = cookieValue.replaceAll("st_oauth_", "ory_hydra_"); + requestHeaders.put("Cookie", cookieValue); + } + return requestHeaders; + } + + private static String transformQueryParamsInURLFromHydra(String redirectTo) { + try { + URL url = new URL(redirectTo); + String query = url.getQuery(); + if (query != null) { + String[] queryParams = query.split("&"); + StringBuilder updatedQuery = new StringBuilder(); + for (String param : queryParams) { + String[] keyValue = param.split("="); + if (keyValue.length > 1 && keyValue[1].startsWith("ory_")) { + updatedQuery.append(keyValue[0]).append("=").append(keyValue[1].replaceFirst("ory_", "st_")).append("&"); + } else { + updatedQuery.append(param).append("&"); + } + } + redirectTo = redirectTo.replace("?" + query, "?" + updatedQuery.toString().trim()); + } + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + + return redirectTo; + } + + public static List transformCookiesFromHydra(List cookies) { + cookies = new ArrayList<>(cookies); // make it modifyable + + for (int i = 0; i < cookies.size(); i++) { + String cookieStr = cookies.get(i); + if (cookieStr.startsWith("ory_hydra_")) { + cookieStr = cookieStr.replaceFirst("ory_hydra_", "st_oauth_"); + cookies.set(i, cookieStr); + } + } + + return cookies; + } + + public static Map transformFormFieldsForHydra(Map bodyParams) { + Map transformedBodyParams = new HashMap<>(); + for (Map.Entry entry : bodyParams.entrySet()) { + String value = entry.getValue(); + if (value.startsWith("st_")) { + value = value.replaceFirst("st_", "ory_"); + } + transformedBodyParams.put(entry.getKey(), value); + } + return transformedBodyParams; + } + + public static JsonElement transformJsonResponseFromHydra(Main main, AppIdentifier appIdentifier, JsonElement jsonResponse) throws InvalidConfigException, TenantOrAppNotFoundException, OAuthAPIException { + if (jsonResponse == null) { + return jsonResponse; + } + + if (jsonResponse.isJsonObject()) { + if (jsonResponse.getAsJsonObject().has("redirect_to")) { + String redirectTo = jsonResponse.getAsJsonObject().get("redirect_to").getAsString(); + redirectTo = transformRedirectUrlFromHydra(main, appIdentifier, redirectTo); + jsonResponse.getAsJsonObject().addProperty("redirect_to", redirectTo); + } + + for (Map.Entry entry : jsonResponse.getAsJsonObject().entrySet()) { + if (entry.getValue().isJsonPrimitive() && entry.getValue().getAsJsonPrimitive().isString()) { + String value = entry.getValue().getAsString(); + if (value.startsWith("ory_")) { + value = value.replaceFirst("ory_", "st_"); + jsonResponse.getAsJsonObject().addProperty(entry.getKey(), value); + } + } + } + } + + return jsonResponse; + } + + private static String transformRedirectUrlFromHydra(Main main, AppIdentifier appIdentifier,String redirectTo) throws InvalidConfigException, TenantOrAppNotFoundException, OAuthAPIException { + String hydraInternalAddress = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main) + .getOauthProviderUrlConfiguredInHydra(); + String hydraBaseUrlForConsentAndLogin = Config + .getConfig(appIdentifier.getAsPublicTenantIdentifier(), main) + .getOauthProviderConsentLoginBaseUrl(); + + if (!redirectTo.startsWith("/")) { + redirectTo = transformQueryParamsInURLFromHydra(redirectTo); + + try { + if (Utils.containsUrl(redirectTo, hydraInternalAddress, true)) { + try { + URL url = new URL(redirectTo); + String query = url.getQuery(); + Map urlQueryParams = new HashMap<>(); + if (query != null) { + String[] pairs = query.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + urlQueryParams.put(pair.substring(0, idx), URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8)); + } + } + String error = urlQueryParams.getOrDefault("error", null); + String errorDescription = urlQueryParams.getOrDefault("error_description", null); + if (error != null) { + throw new OAuthAPIException(error, errorDescription, 400); + } + redirectTo = redirectTo.replace(hydraInternalAddress, "{apiDomain}"); + + // path to hydra starts with /oauth2 while on the SDK it would be /oauth + redirectTo = redirectTo.replace("oauth2/", "oauth/"); + + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + } else if (Utils.containsUrl(redirectTo, hydraBaseUrlForConsentAndLogin, true)) { + redirectTo = redirectTo.replace(hydraBaseUrlForConsentAndLogin, "{apiDomain}"); + } + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + } + + return redirectTo; + } + + public static Map> transformResponseHeadersFromHydra(Main main, AppIdentifier appIdentifier, + Map> headers) + throws InvalidConfigException, TenantOrAppNotFoundException, OAuthAPIException { + if (headers == null) { + return headers; + } + + headers = new HashMap<>(headers); // make it modifyable + + final String LOCATION_HEADER_NAME = "Location"; + if (headers.containsKey(LOCATION_HEADER_NAME)) { + // Transform url in Location header + String redirectTo = headers.get(LOCATION_HEADER_NAME).get(0); + redirectTo = transformRedirectUrlFromHydra(main, appIdentifier, redirectTo); + headers.put(LOCATION_HEADER_NAME, List.of(redirectTo)); + } + + final String COOKIES_HEADER_NAME = "Set-Cookie"; + if (headers.containsKey(COOKIES_HEADER_NAME)) { + // Cookie transformation + List cookies = headers.get(COOKIES_HEADER_NAME); + cookies = Transformations.transformCookiesFromHydra(cookies); + headers.put(COOKIES_HEADER_NAME, cookies); + } + + return headers; + } + + public static JsonObject transformJsonForHydra(JsonObject jsonInput) { + JsonObject transformedJsonInput = new JsonObject(); + for (Map.Entry entry : jsonInput.entrySet()) { + String key = entry.getKey(); + com.google.gson.JsonElement value = entry.getValue(); + if (value.isJsonPrimitive() && ((com.google.gson.JsonPrimitive) value).isString()) { + String stringValue = ((com.google.gson.JsonPrimitive) value).getAsString(); + if (stringValue.startsWith("st_")) { + stringValue = stringValue.replaceFirst("st_", "ory_"); + transformedJsonInput.addProperty(key, stringValue); + } else { + transformedJsonInput.add(key, value); + } + } else { + transformedJsonInput.add(key, value); + } + } + return transformedJsonInput; + } + + public static void transformExt(JsonObject payload) { + if (payload.has("ext")) { + JsonObject ext = payload.get("ext").getAsJsonObject(); + for (String prop : EXT_PROPS) { + if (ext.has(prop)) { + payload.addProperty(prop, ext.get(prop).getAsString()); + ext.remove(prop); + } + } + } + } + + public static void applyClientPropsWhiteList(JsonObject payload) { + List propsToRemove = new ArrayList<>(); + + for (Map.Entry entry : payload.entrySet()) { + if (!CLIENT_PROPS.contains(entry.getKey())) { + propsToRemove.add(entry.getKey()); + } + } + + for (String prop : propsToRemove) { + payload.remove(prop); + } + } +} diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java similarity index 83% rename from src/main/java/io/supertokens/oauth/exceptions/OAuthException.java rename to src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java index 890cb02cb..dc80be702 100644 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthException.java +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java @@ -16,18 +16,18 @@ package io.supertokens.oauth.exceptions; -import java.io.Serial; - -public class OAuthException extends Exception{ - @Serial +public class OAuthAPIException extends Exception { private static final long serialVersionUID = 1836718299845759897L; public final String error; public final String errorDescription; + public final int statusCode; - public OAuthException(String error, String errorDescription){ + public OAuthAPIException(String error, String errorDescription, int statusCode) { super(error); + this.error = error; this.errorDescription = errorDescription; + this.statusCode = statusCode; } } diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java deleted file mode 100644 index 91634a198..000000000 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.oauth.exceptions; - -import java.io.Serial; - -public class OAuthAPIInvalidInputException extends OAuthException{ - - @Serial - private static final long serialVersionUID = 665027786586190611L; - - public OAuthAPIInvalidInputException(String error, String errorDescription) { - super(error, errorDescription); - } -} diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthAuthException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthAuthException.java deleted file mode 100644 index ba850d287..000000000 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthAuthException.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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.oauth.exceptions; - -public class OAuthAuthException extends OAuthException{ - private static final long serialVersionUID = 1836718299845759897L; - - public OAuthAuthException(String error, String errorDescription) { - super(error, errorDescription); - } -} diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java index 77b3df4f7..94ddff968 100644 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java @@ -18,11 +18,7 @@ import java.io.Serial; -public class OAuthClientNotFoundException extends OAuthException{ +public class OAuthClientNotFoundException extends Exception { @Serial private static final long serialVersionUID = 1412853176388698991L; - - public OAuthClientNotFoundException(String error, String errorDescription) { - super(error, errorDescription); - } } diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientUpdateException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthClientUpdateException.java deleted file mode 100644 index bc03389f2..000000000 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientUpdateException.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.oauth.exceptions; - -import java.io.Serial; - -public class OAuthClientUpdateException extends OAuthException{ - @Serial - private static final long serialVersionUID = -5191044905397936167L; - - public OAuthClientUpdateException(String error, String errorDescription) { - super(error, errorDescription); - } -} diff --git a/src/main/java/io/supertokens/utils/Utils.java b/src/main/java/io/supertokens/utils/Utils.java index 07ea2e5ff..9775bb0bb 100644 --- a/src/main/java/io/supertokens/utils/Utils.java +++ b/src/main/java/io/supertokens/utils/Utils.java @@ -51,6 +51,7 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.util.Base64.Decoder; diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index 45c262a64..f283ca8b2 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -39,8 +39,7 @@ import io.supertokens.webserver.api.multitenancy.*; import io.supertokens.webserver.api.multitenancy.thirdparty.CreateOrUpdateThirdPartyConfigAPI; import io.supertokens.webserver.api.multitenancy.thirdparty.RemoveThirdPartyConfigAPI; -import io.supertokens.webserver.api.oauth.OAuthAuthAPI; -import io.supertokens.webserver.api.oauth.OAuthClientsAPI; +import io.supertokens.webserver.api.oauth.*; import io.supertokens.webserver.api.passwordless.*; import io.supertokens.webserver.api.session.*; import io.supertokens.webserver.api.thirdparty.GetUsersByEmailAPI; @@ -270,7 +269,26 @@ private void setupRoutes() { addAPI(new GetTenantCoreConfigForDashboardAPI(main)); addAPI(new OAuthAuthAPI(main)); - addAPI(new OAuthClientsAPI(main)); + addAPI(new OAuthTokenAPI(main)); + addAPI(new CreateUpdateOrGetOAuthClientAPI(main)); + addAPI(new OAuthClientListAPI(main)); + addAPI(new RemoveOAuthClientAPI(main)); + + addAPI(new OAuthGetAuthConsentRequestAPI(main)); + addAPI(new OAuthAcceptAuthConsentRequestAPI(main)); + addAPI(new OAuthRejectAuthConsentRequestAPI(main)); + addAPI(new OAuthGetAuthLoginRequestAPI(main)); + addAPI(new OAuthAcceptAuthLoginRequestAPI(main)); + addAPI(new OAuthRejectAuthLoginRequestAPI(main)); + addAPI(new OAuthGetAuthLogoutRequestAPI(main)); + addAPI(new OAuthAcceptAuthLogoutRequestAPI(main)); + addAPI(new OAuthRejectAuthLogoutRequestAPI(main)); + addAPI(new OAuthTokenIntrospectAPI(main)); + + addAPI(new RevokeOAuthTokenAPI(main)); + addAPI(new RevokeOAuthTokensAPI(main)); + addAPI(new RevokeOAuthSessionAPI(main)); + addAPI(new OAuthLogoutAPI(main)); StandardContext context = tomcatReference.getContext(); Tomcat tomcat = tomcatReference.getTomcat(); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java new file mode 100644 index 000000000..7203f0949 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -0,0 +1,196 @@ +/* + * 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.webserver.api.oauth; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +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.OAuth; +import io.supertokens.oauth.Transformations; +import io.supertokens.oauth.exceptions.OAuthAPIException; +import io.supertokens.oauth.exceptions.OAuthClientNotFoundException; +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.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class CreateUpdateOrGetOAuthClientAPI extends WebserverAPI { + @Override + public String getPath() { + return "/recipe/oauth/clients"; + } + + public CreateUpdateOrGetOAuthClientAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + String clientId = InputParser.getQueryParamOrThrowError(req, "clientId", false); + + try { + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + clientId, // clientIdToCheck + "/admin/clients/" + clientId, // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + new HashMap<>() + ); + if (response != null) { + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + // Defaults that we require + input.addProperty("accessTokenStrategy", "jwt"); + input.addProperty("skipConsent", true); + input.addProperty("subjectType", "public"); + input.addProperty("clientId", "stcl_" + UUID.randomUUID()); + + boolean isClientCredentialsOnly = input.has("grantTypes") && + input.get("grantTypes").isJsonArray() && + input.get("grantTypes").getAsJsonArray().size() == 1 && + input.get("grantTypes").getAsJsonArray().get(0).getAsString().equals("client_credentials"); + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + input.addProperty("owner", appIdentifier.getAppId()); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPOST( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/clients", // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + input, // jsonBody + new HashMap<>() // headers + ); + if (response != null) { + String clientId = response.jsonResponse.getAsJsonObject().get("clientId").getAsString(); + + try { + OAuth.addOrUpdateClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId, isClientCredentialsOnly); + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } + + @Override + 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( + main, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + clientId, + "/admin/clients/" + clientId, + true, true, queryParams, null); + + JsonObject existingConfig = response.jsonResponse.getAsJsonObject(); + for (Map.Entry entry : existingConfig.entrySet()) { + String key = entry.getKey(); + if (!input.has(key)) { + input.add(key, entry.getValue()); + } + } + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { + throw new ServletException(e); + } catch (OAuthClientNotFoundException | OAuthAPIException e) { + // ignore since the PUT API will throw one of this error later on + } + + try { + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + clientId, // clientIdToCheck + "/admin/clients/" + clientId, + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + new HashMap<>(), // queryParams + input, // jsonBody + new HashMap<>() // headers + ); + + if (response != null) { + try { + OAuth.addOrUpdateClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId, isClientCredentialsOnly); + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java new file mode 100644 index 000000000..f9d28c9fc --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java @@ -0,0 +1,87 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.HashMap; +import java.util.UUID; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthAcceptAuthConsentRequestAPI extends WebserverAPI { + + public OAuthAcceptAuthConsentRequestAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/consent/accept"; + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String iss = InputParser.parseStringOrThrowError(input, "iss", false); + String tId = InputParser.parseStringOrThrowError(input, "tId", false); + String rsub = InputParser.parseStringOrThrowError(input, "rsub", false); + String sessionHandle = InputParser.parseStringOrThrowError(input, "sessionHandle", false); + JsonObject initialAccessTokenPayload = InputParser.parseJsonObjectOrThrowError(input, "initialAccessTokenPayload", false); + JsonObject initialIdTokenPayload = InputParser.parseJsonObjectOrThrowError(input, "initialIdTokenPayload", false); + + JsonObject accessToken = new JsonObject(); + accessToken.addProperty("iss", iss); + accessToken.addProperty("tId", tId); + accessToken.addProperty("rsub", rsub); + accessToken.addProperty("sessionHandle", sessionHandle); + accessToken.add("initialPayload", initialAccessTokenPayload); + + JsonObject idToken = new JsonObject(); + idToken.add("initialPayload", initialIdTokenPayload); + accessToken.addProperty("gid", UUID.randomUUID().toString()); + + // remove the above from input + input.remove("iss"); + input.remove("tId"); + input.remove("rsub"); + input.remove("sessionHandle"); + input.remove("initialAccessTokenPayload"); + input.remove("initialIdTokenPayload"); + + JsonObject session = new JsonObject(); + session.add("access_token", accessToken); + session.add("id_token", idToken); + input.add("session", session); + + try { + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck + "/admin/oauth2/auth/requests/consent/accept", // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + input, // jsonBody + new HashMap<>() // headers + ); + + if (response != null) { + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java new file mode 100644 index 000000000..792f01539 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java @@ -0,0 +1,56 @@ +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.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; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthAcceptAuthLoginRequestAPI extends WebserverAPI { + + public OAuthAcceptAuthLoginRequestAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/login/accept"; + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + try { + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck + "/admin/oauth2/auth/requests/login/accept", + true, + true, + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + input, // jsonBody + new HashMap<>() // headers + ); + + if (response != null) { + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java new file mode 100644 index 000000000..7d07c254d --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java @@ -0,0 +1,56 @@ +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.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthAcceptAuthLogoutRequestAPI extends WebserverAPI { + + public OAuthAcceptAuthLogoutRequestAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/logout/accept"; + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + try { + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck + "/admin/oauth2/auth/requests/logout/accept", // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), // queryParams + input, // jsonBody + new HashMap<>() // headers + ); + + if (response != null) { + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} 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 559663d14..7ff44dcfc 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -16,19 +16,18 @@ package io.supertokens.webserver.api.oauth; -import com.google.gson.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + import io.supertokens.Main; -import io.supertokens.httpRequest.HttpResponseException; import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; import io.supertokens.oauth.OAuth; -import io.supertokens.oauth.exceptions.OAuthAuthException; 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.oauth.OAuthAuthResponse; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -36,20 +35,16 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import java.io.Serial; -import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; public class OAuthAuthAPI extends WebserverAPI { - @Serial - private static final long serialVersionUID = -8734479943734920904L; - public OAuthAuthAPI(Main main) { super(main, RECIPE_ID.OAUTH.toString()); } - private static final List REQUIRED_FIELDS_FOR_POST = Arrays.asList(new String[]{"clientId", "responseType"}); - @Override public String getPath() { return "/recipe/oauth/auth"; @@ -57,39 +52,71 @@ public String getPath() { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - InputParser.throwErrorOnMissingRequiredField(input, REQUIRED_FIELDS_FOR_POST); + JsonObject params = InputParser.parseJsonObjectOrThrowError(input, "params", false); + String cookies = InputParser.parseStringOrThrowError(input, "cookies", true); + + // These optional stuff will be used in case of implicit flow + JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "access_token", true); + JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "id_token", true); + String iss = InputParser.parseStringOrThrowError(input, "iss", true); + Boolean useStaticKeyInput = InputParser.parseBooleanOrThrowError(input, "useStaticSigningKey", true); + boolean useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); + + Map queryParams = params.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue().getAsString() + )); + + Map headers = new HashMap<>(); + + if (cookies != null) { + headers.put("Cookie", cookies); + } try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - OAuthAuthResponse authResponse = OAuth.getAuthorizationUrl(super.main, appIdentifier, storage, - input); - JsonObject response = new JsonObject(); - response.addProperty("redirectTo", authResponse.redirectTo); + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( + main, req, resp, + appIdentifier, + storage, + queryParams.get("client_id"), // clientIdToCheck + "/oauth2/auth", // proxyPath + false, // proxyToAdmin + false, // camelToSnakeCaseConversion + queryParams, + headers + ); - JsonArray jsonCookies = new JsonArray(); - if (authResponse.cookies != null) { - for(String cookie : authResponse.cookies){ - jsonCookies.add(new JsonPrimitive(cookie)); + if (response != null) { + if (response.headers == null || !response.headers.containsKey("Location")) { + throw new IllegalStateException("Invalid response from hydra"); } - } - response.add("cookies", jsonCookies); - response.addProperty("status", "OK"); - super.sendJsonResponse(200, response, resp); + + String redirectTo = response.headers.get("Location").get(0); - } catch (OAuthAuthException authException) { + redirectTo = OAuth.transformTokensInAuthRedirect(main, appIdentifier, storage, redirectTo, iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); + List responseCookies = response.headers.get("Set-Cookie"); - JsonObject errorResponse = new JsonObject(); - errorResponse.addProperty("error", authException.error); - errorResponse.addProperty("errorDescription", authException.errorDescription); - errorResponse.addProperty("status", "OAUTH2_AUTH_ERROR"); - super.sendJsonResponse(200, errorResponse, resp); + JsonObject finalResponse = new JsonObject(); + finalResponse.addProperty("redirectTo", redirectTo); + + JsonArray jsonCookies = new JsonArray(); + if (responseCookies != null) { + for (String cookie : responseCookies) { + jsonCookies.add(new JsonPrimitive(cookie)); + } + } + + finalResponse.add("cookies", jsonCookies); + finalResponse.addProperty("status", "OK"); + + super.sendJsonResponse(200, finalResponse, resp); + } - } catch (TenantOrAppNotFoundException | InvalidConfigException | HttpResponseException | - StorageQueryException | BadPermissionException e) { + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java new file mode 100644 index 000000000..00eff45d3 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java @@ -0,0 +1,111 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +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; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthClientListAPI extends WebserverAPI { + + public OAuthClientListAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/clients/list"; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + Map queryParams = OAuthProxyHelper.defaultGetQueryParamsFromRequest(req); + queryParams.put("owner", appIdentifier.getAppId()); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/clients", // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + queryParams, + new HashMap<>() // headers + ); + + if (response != null) { + JsonObject finalResponse = new JsonObject(); + finalResponse.addProperty("status", "OK"); + + // Filter out the clients for app + List clientIds; + try { + clientIds = OAuth.listClientIds(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req)); + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException 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())) { + clients.add(clientElem); + } + } + + finalResponse.add("clients", clients); + + // pagination + List linkHeader = response.headers.get("Link"); + if (linkHeader != null && !linkHeader.isEmpty()) { + for (String nextLink : linkHeader.get(0).split(",")) { + if (!nextLink.contains("rel=\"next\"")) { + continue; + } + + String pageToken = null; + if (nextLink.contains("page_token=")) { + int startIndex = nextLink.indexOf("page_token=") + "page_token=".length(); + int endIndex = nextLink.indexOf('>', startIndex); + if (endIndex != -1) { + pageToken = nextLink.substring(startIndex, endIndex); + } + } + if (pageToken != null) { + finalResponse.addProperty("nextPaginationToken", pageToken); + } + } + } + + super.sendJsonResponse(200, finalResponse, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientsAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientsAPI.java deleted file mode 100644 index a0c269ec1..000000000 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientsAPI.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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.webserver.api.oauth; - -import com.google.gson.JsonObject; -import io.supertokens.Main; -import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.OAuth; -import io.supertokens.oauth.exceptions.OAuthClientNotFoundException; -import io.supertokens.oauth.exceptions.OAuthAPIInvalidInputException; -import io.supertokens.oauth.exceptions.OAuthClientUpdateException; -import io.supertokens.oauth.exceptions.OAuthException; -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.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.io.Serial; -import java.lang.reflect.InvocationTargetException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.List; - -public class OAuthClientsAPI extends WebserverAPI { - - @Serial - private static final long serialVersionUID = -4482427281337641246L; - - private static final List REQUIRED_INPUT_FIELDS_FOR_POST = Arrays.asList(new String[]{"clientName", "scope"}); - private static final List REQUIRED_INPUT_FIELDS_FOR_PATCH = Arrays.asList(new String[]{"clientId"}); - public static final String OAUTH2_CLIENT_NOT_FOUND_ERROR = "OAUTH2_CLIENT_NOT_FOUND_ERROR"; - public static final String OAUTH2_CLIENT_UPDATE_ERROR = "OAUTH2_CLIENT_UPDATE_ERROR"; - - @Override - public String getPath() { - return "/recipe/oauth/clients"; - } - public OAuthClientsAPI(Main main){ - super(main, RECIPE_ID.OAUTH.toString()); - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - - JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - InputParser.throwErrorOnMissingRequiredField(input, REQUIRED_INPUT_FIELDS_FOR_POST); - - try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - - JsonObject response = OAuth.registerOAuthClient(super.main, appIdentifier, storage, input); - JsonObject postResponseBody = new JsonObject(); - postResponseBody.addProperty("status", "OK"); - postResponseBody.add("client", response); - sendJsonResponse(200, postResponseBody, resp); - - } catch (OAuthAPIInvalidInputException registerException) { - - throw new ServletException(new BadRequestException(registerException.error + " - " + registerException.errorDescription)); - - } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException - | NoSuchAlgorithmException | StorageQueryException e) { - throw new ServletException(e); - } - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - String clientId = InputParser.getQueryParamOrThrowError(req, "clientId", false); - - try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - - JsonObject client = OAuth.loadOAuthClient(main, appIdentifier, storage, clientId); - - JsonObject response = new JsonObject(); - response.addProperty("status", "OK"); - response.add("client", client); - sendJsonResponse(200, response, resp); - - } catch (OAuthClientNotFoundException e) { - JsonObject errorResponse = createJsonFromException(e, OAUTH2_CLIENT_NOT_FOUND_ERROR); - sendJsonResponse(200, errorResponse, resp); - - } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException - | StorageQueryException e){ - throw new ServletException(e); - } - } - - @Override - protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - JsonObject requestBody = InputParser.parseJsonObjectOrThrowError(req); - - String clientId = InputParser.parseStringOrThrowError(requestBody, "clientId", false); - - try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - - OAuth.deleteOAuthClient(main, appIdentifier, storage, clientId); - JsonObject responseBody = new JsonObject(); - responseBody.addProperty("status", "OK"); - sendJsonResponse(200, responseBody, resp); - - } catch (OAuthClientNotFoundException e) { - JsonObject errorResponse = createJsonFromException(e, OAUTH2_CLIENT_NOT_FOUND_ERROR); - sendJsonResponse(200, errorResponse, resp); - - } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException - | StorageQueryException e){ - throw new ServletException(e); - } - } - - @Override - protected void doPatch(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - InputParser.throwErrorOnMissingRequiredField(input, REQUIRED_INPUT_FIELDS_FOR_PATCH); - - try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - - JsonObject response = OAuth.updateOauthClient(super.main, appIdentifier, storage, input); - JsonObject postResponseBody = new JsonObject(); - postResponseBody.addProperty("status", "OK"); - postResponseBody.add("client", response); - sendJsonResponse(200, postResponseBody, resp); - - } catch (OAuthAPIInvalidInputException exception) { - throw new ServletException(new BadRequestException(exception.error + " - " + exception.errorDescription)); - } catch (OAuthClientUpdateException updateException) { - //for errors with the update from hydra, which are not reported back as invalid input errors - throw new ServletException(updateException); - - } catch (OAuthClientNotFoundException clientNotFoundException) { - JsonObject errorResponse = createJsonFromException(clientNotFoundException, OAUTH2_CLIENT_NOT_FOUND_ERROR); - sendJsonResponse(200, errorResponse, resp); - - } catch (TenantOrAppNotFoundException | InvalidConfigException | StorageQueryException | - InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException - | BadPermissionException ex) { - throw new ServletException(ex); - } - } - - private JsonObject createJsonFromException(OAuthException exception, String status){ - JsonObject errorResponse = new JsonObject(); - errorResponse.addProperty("error", exception.error); - errorResponse.addProperty("errorDescription", exception.errorDescription); - errorResponse.addProperty("status", status); - return errorResponse; - } -} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java new file mode 100644 index 000000000..37f66ae6e --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java @@ -0,0 +1,54 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.HashMap; + +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.Transformations; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthGetAuthConsentRequestAPI extends WebserverAPI { + + public OAuthGetAuthConsentRequestAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/consent"; + } + + @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/consent", // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + new HashMap<>() // headers + ); + + if (response != null) { + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject()); + + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java new file mode 100644 index 000000000..cc3c06b2c --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java @@ -0,0 +1,54 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.HashMap; + +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.Transformations; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthGetAuthLoginRequestAPI extends WebserverAPI { + + public OAuthGetAuthLoginRequestAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/login"; + } + + @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 + ); + + if (response != null) { + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject()); + + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java new file mode 100644 index 000000000..143e505b0 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java @@ -0,0 +1,54 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.HashMap; + +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.Transformations; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthGetAuthLogoutRequestAPI extends WebserverAPI { + + public OAuthGetAuthLogoutRequestAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/logout"; + } + + @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/logout", // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + new HashMap<>() // headers + ); + + if (response != null) { + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject()); + + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java new file mode 100644 index 000000000..8e6626733 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java @@ -0,0 +1,73 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.exceptions.OAuthAPIException; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthLogoutAPI extends WebserverAPI { + public OAuthLogoutAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/sessions/logout"; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + Map queryParams = OAuthProxyHelper.defaultGetQueryParamsFromRequest(req); + OAuth.verifyIdTokenHintClientIdAndUpdateQueryParamsForLogout(main, appIdentifier, storage, queryParams); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( + main, req, resp, + appIdentifier, + storage, + queryParams.get("clientId"), // clientIdToCheck + "/oauth2/sessions/logout", // proxyPath + false, // proxyToAdmin + true, // camelToSnakeCaseConversion + queryParams, + new HashMap<>() // headers + ); + + if (response != null) { + JsonObject finalResponse = new JsonObject(); + String redirectTo = response.headers.get("Location").get(0); + + finalResponse.addProperty("status", "OK"); + finalResponse.addProperty("redirectTo", redirectTo); + + super.sendJsonResponse(200, finalResponse, resp); + } + + } catch (OAuthAPIException e) { + OAuthProxyHelper.handleOAuthAPIException(resp, e); + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | UnsupportedJWTSigningAlgorithmException | StorageTransactionLogicException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java new file mode 100644 index 000000000..6764ae05d --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java @@ -0,0 +1,145 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.io.Serial; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +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.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.exceptions.OAuthAPIException; +import io.supertokens.oauth.exceptions.OAuthClientNotFoundException; +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 jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +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 { + try { + return OAuth.doOAuthProxyGET(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, headers); + + } catch (OAuthClientNotFoundException e) { + handleOAuthClientNotFoundException(resp); + } catch (OAuthAPIException e) { + handleOAuthAPIException(resp, e); + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + 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 { + try { + return OAuth.doOAuthProxyFormPOST(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, formFields, headers); + } catch (OAuthClientNotFoundException e) { + handleOAuthClientNotFoundException(resp); + } catch (OAuthAPIException e) { + handleOAuthAPIException(resp, e); + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + 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 { + try { + return OAuth.doOAuthProxyJsonPOST(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); + } catch (OAuthClientNotFoundException e) { + handleOAuthClientNotFoundException(resp); + } catch (OAuthAPIException e) { + handleOAuthAPIException(resp, e); + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + 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 { + + try { + return OAuth.doOAuthProxyJsonPUT(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); + } catch (OAuthClientNotFoundException e) { + handleOAuthClientNotFoundException(resp); + } catch (OAuthAPIException e) { + handleOAuthAPIException(resp, e); + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + 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 { + try { + return OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); + } catch (OAuthClientNotFoundException e) { + handleOAuthClientNotFoundException(resp); + } catch (OAuthAPIException e) { + handleOAuthAPIException(resp, e); + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + return null; + } + + public static Map defaultGetQueryParamsFromRequest(HttpServletRequest req) { + Map queryParams = new HashMap<>(); + + String queryString = req.getQueryString(); + if (queryString != null) { + String[] queryParamsParts = queryString.split("&"); + for (String queryParam : queryParamsParts) { + String[] keyValue = queryParam.split("="); + if (keyValue.length == 2) { + queryParams.put(keyValue[0], URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8)); + } + } + } + + return queryParams; + } + + private static void handleOAuthClientNotFoundException(HttpServletResponse resp) throws IOException { + JsonObject response = new JsonObject(); + response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); + + resp.setStatus(200); + resp.setHeader("Content-Type", "application/json; charset=UTF-8"); + resp.getWriter().println(response.toString()); + } + + public static void handleOAuthAPIException(HttpServletResponse resp, OAuthAPIException e) throws IOException { + JsonObject response = new JsonObject(); + response.addProperty("status", "OAUTH_ERROR"); + response.addProperty("error", e.error); + response.addProperty("errorDescription", e.errorDescription); + response.addProperty("statusCode", e.statusCode); + + resp.setStatus(200); + resp.setHeader("Content-Type", "application/json; charset=UTF-8"); + resp.getWriter().println(response.toString()); + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java new file mode 100644 index 000000000..ef4fed870 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java @@ -0,0 +1,57 @@ +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.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthRejectAuthConsentRequestAPI extends WebserverAPI { + + public OAuthRejectAuthConsentRequestAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/consent/reject"; + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + try { + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck + "/admin/oauth2/auth/requests/consent/reject", // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), // queryParams + input, // jsonBody + new HashMap<>() // headers + ); + + if (response != null) { + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java new file mode 100644 index 000000000..6462d358c --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java @@ -0,0 +1,58 @@ +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.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthRejectAuthLoginRequestAPI extends WebserverAPI { + + public OAuthRejectAuthLoginRequestAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/login/reject"; + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + try { + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck + "/admin/oauth2/auth/requests/login/reject", // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), // queryParams + input, // jsonBody + new HashMap<>() // headers + ); + + if (response != null) { + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java new file mode 100644 index 000000000..10252c230 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java @@ -0,0 +1,57 @@ +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.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthRejectAuthLogoutRequestAPI extends WebserverAPI { + + public OAuthRejectAuthLogoutRequestAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/logout/reject"; + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + try { + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck + "/admin/oauth2/auth/requests/logout/reject", // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + input, // jsonBody + new HashMap<>() // headers + ); + + if (response != null) { + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java new file mode 100644 index 000000000..843fc0245 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -0,0 +1,166 @@ +/* + * 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.webserver.api.oauth; + +import com.auth0.jwt.exceptions.JWTCreationException; +import com.google.gson.*; +import io.supertokens.Main; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.exceptions.OAuthAPIException; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.session.jwt.JWT.JWTException; +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.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.HashMap; +import java.util.Map; + +public class OAuthTokenAPI extends WebserverAPI { + + public OAuthTokenAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/token"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String iss = InputParser.parseStringOrThrowError(input, "iss", false); // input validation + JsonObject bodyFromSDK = InputParser.parseJsonObjectOrThrowError(input, "inputBody", false); + + String grantType = InputParser.parseStringOrThrowError(bodyFromSDK, "grant_type", false); + JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "access_token", "authorization_code".equals(grantType)); + JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "id_token", "authorization_code".equals(grantType)); + + // useStaticKeyInput defaults to true, so we check if it has been explicitly set to false + Boolean useStaticKeyInput = InputParser.parseBooleanOrThrowError(input, "useStaticSigningKey", true); + boolean useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); + + String authorizationHeader = InputParser.parseStringOrThrowError(input, "authorizationHeader", true); + + Map headers = new HashMap<>(); + if (authorizationHeader != null) { + headers.put("Authorization", authorizationHeader); + } + + Map formFields = new HashMap<>(); + for (Map.Entry entry : bodyFromSDK.entrySet()) { + formFields.put(entry.getKey(), entry.getValue().getAsString()); + } + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + // check if the refresh token is valid + if (grantType.equals("refresh_token")) { + String refreshToken = InputParser.parseStringOrThrowError(bodyFromSDK, "refresh_token", false); + + Map formFieldsForTokenIntrospect = new HashMap<>(); + formFieldsForTokenIntrospect.put("token", refreshToken); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/oauth2/introspect", // pathProxy + true, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFieldsForTokenIntrospect, + new HashMap<>() // headers + ); + + if (response == null) { + return; // proxy helper would have sent the error response + } + + JsonObject refreshTokenPayload = response.jsonResponse.getAsJsonObject(); + + try { + OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, refreshTokenPayload, refreshToken); + } catch (StorageQueryException | TenantOrAppNotFoundException | + FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + + if (!refreshTokenPayload.get("active").getAsBoolean()) { + // this is what ory would return for an invalid token + OAuthProxyHelper.handleOAuthAPIException(resp, new OAuthAPIException( + "token_inactive", "Token is inactive because it is malformed, expired or otherwise invalid. Token validation failed.", 401 + )); + return; + } + } + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + formFields.get("client_id"), // clientIdToCheck + "/oauth2/token", // proxyPath + false, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFields, + headers // headers + ); + + if (response != null) { + try { + response.jsonResponse = OAuth.transformTokens(super.main, appIdentifier, storage, response.jsonResponse.getAsJsonObject(), iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); + + if (grantType.equals("client_credentials")) { + try { + OAuth.addM2MToken(main, appIdentifier, storage, response.jsonResponse.getAsJsonObject().get("access_token").getAsString()); + } catch (Exception e) { + // ignore + } + } + + } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { + throw new ServletException(e); + } + + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java new file mode 100644 index 000000000..1bf281f8e --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java @@ -0,0 +1,108 @@ +/* + * 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.webserver.api.oauth; + +import com.google.gson.*; +import io.supertokens.Main; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class OAuthTokenIntrospectAPI extends WebserverAPI { + + public OAuthTokenIntrospectAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/introspect"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String token = InputParser.parseStringOrThrowError(input, "token", false); + + if (token.startsWith("st_rt_")) { + Map formFields = new HashMap<>(); + for (Map.Entry entry : input.entrySet()) { + formFields.put(entry.getKey(), entry.getValue().getAsString()); + } + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/oauth2/introspect", // pathProxy + true, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFields, + new HashMap<>() // headers + ); + + if (response != null) { + JsonObject finalResponse = response.jsonResponse.getAsJsonObject(); + + try { + OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, finalResponse, token); + } catch (StorageQueryException | TenantOrAppNotFoundException | + FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + + finalResponse.addProperty("status", "OK"); + super.sendJsonResponse(200, finalResponse, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } else { + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + JsonObject response = OAuth.introspectAccessToken(main, appIdentifier, storage, token); + super.sendJsonResponse(200, response, resp); + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { + throw new ServletException(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 new file mode 100644 index 000000000..6844eff81 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java @@ -0,0 +1,85 @@ +/* + * 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.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; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class RemoveOAuthClientAPI extends WebserverAPI { + + public RemoveOAuthClientAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/clients/remove"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String clientId = InputParser.parseStringOrThrowError(input, "clientId", false); + + try { + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonDELETE( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + clientId, // clientIdToCheck + "/admin/clients/" + clientId, // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + new HashMap<>(), // queryParams + new JsonObject(), // jsonBody + new HashMap<>() // headers + ); + + 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"); + + super.sendJsonResponse(200, finalResponse, resp); + } + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthSessionAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthSessionAPI.java new file mode 100644 index 000000000..ef54c7ee6 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthSessionAPI.java @@ -0,0 +1,50 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class RevokeOAuthSessionAPI extends WebserverAPI { + public RevokeOAuthSessionAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/session/revoke"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + String sessionHandle = InputParser.parseStringOrThrowError(input, "sessionHandle", false); + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + OAuth.revokeSessionHandle(main, appIdentifier, storage, sessionHandle); + + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + super.sendJsonResponse(200, response, resp); + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java new file mode 100644 index 000000000..b71ddd6d6 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java @@ -0,0 +1,141 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class RevokeOAuthTokenAPI extends WebserverAPI { + public RevokeOAuthTokenAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/token/revoke"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + String token = InputParser.parseStringOrThrowError(input, "token", false); + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + if (token.startsWith("st_rt_")) { + String gid = null; + long exp = -1; + { + // introspect token to get gid + Map formFields = new HashMap<>(); + formFields.put("token", token); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/oauth2/introspect", // pathProxy + true, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFields, + new HashMap<>() // headers + ); + + if (response != null) { + JsonObject finalResponse = response.jsonResponse.getAsJsonObject(); + + try { + OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, finalResponse, token); + if (finalResponse.get("active").getAsBoolean()) { + gid = finalResponse.get("gid").getAsString(); + exp = finalResponse.get("exp").getAsLong(); + } + } catch (StorageQueryException | TenantOrAppNotFoundException | + FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + } + } + + // revoking refresh token + String clientId = InputParser.parseStringOrThrowError(input, "client_id", false); + String clientSecret = InputParser.parseStringOrThrowError(input, "client_secret", true); + + String authorizationHeader = InputParser.parseStringOrThrowError(input, "authorizationHeader", true); + + Map headers = new HashMap<>(); + if (authorizationHeader != null) { + headers.put("Authorization", authorizationHeader); + } + + Map formFields = new HashMap<>(); + formFields.put("token", token); + formFields.put("client_id", clientId); + if (clientSecret != null) { + formFields.put("client_secret", clientSecret); + } + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + null, //clientIdToCheck + "/oauth2/revoke", // path + false, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFields, // formFields + headers // headers + ); + + if (response != null) { + // Success response would mean that the clientId/secret has been validated + if (gid != null) { + try { + OAuth.revokeRefreshToken(main, appIdentifier, storage, gid, exp); + } catch (StorageQueryException | NoSuchAlgorithmException e) { + throw new ServletException(e); + } + } + + JsonObject finalResponse = new JsonObject(); + finalResponse.addProperty("status", "OK"); + super.sendJsonResponse(200, finalResponse, resp); + } + } else { + // revoking access token + OAuth.revokeAccessToken(main, appIdentifier, storage, token); + + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + super.sendJsonResponse(200, response, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | + UnsupportedJWTSigningAlgorithmException | StorageTransactionLogicException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java new file mode 100644 index 000000000..6aef1c3ef --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java @@ -0,0 +1,70 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class RevokeOAuthTokensAPI extends WebserverAPI { + + public RevokeOAuthTokensAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/tokens/revoke"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String clientId = InputParser.parseStringOrThrowError(input, "client_id", false); + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + OAuth.revokeTokensForClientId(main, appIdentifier, storage, clientId); + + Map queryParams = new HashMap<>(); + queryParams.put("client_id", clientId); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonDELETE( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/oauth2/tokens", // proxyPath + true, // proxyToAdmin + false, // camelToSnakeCaseConversion + queryParams, // queryParams + new JsonObject(), // jsonInput + new HashMap<>() // headers + ); + + if (response != null) { + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException e) { + throw new ServletException(e); + } + } +} diff --git a/src/test/java/io/supertokens/test/HttpRequestTest.java b/src/test/java/io/supertokens/test/HttpRequestTest.java index 98213b3ab..c733c4ea7 100644 --- a/src/test/java/io/supertokens/test/HttpRequestTest.java +++ b/src/test/java/io/supertokens/test/HttpRequestTest.java @@ -16,32 +16,32 @@ package io.supertokens.test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; + +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.JsonObject; import com.google.gson.JsonParser; + import io.supertokens.ProcessState; import io.supertokens.httpRequest.HttpRequest; import io.supertokens.httpRequest.HttpResponseException; import io.supertokens.webserver.Webserver; import io.supertokens.webserver.WebserverAPI; -import jakarta.servlet.http.Cookie; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestRule; - import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.BufferedReader; -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -import static org.junit.Assert.*; - public class HttpRequestTest { @Rule @@ -739,56 +739,4 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } - - @Test - public void getRequestTestWithHeaders() throws Exception { - String[] args = {"../"}; - - TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - // api to check getRequestWithParams - Webserver.getInstance(process.getProcess()).addAPI(new WebserverAPI(process.getProcess(), "") { - - private static final long serialVersionUID = 1L; - - @Override - protected boolean checkAPIKey(HttpServletRequest req) { - return false; - } - - @Override - public String getPath() { - return "/getTestWithHeaders"; - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - Cookie cookie1 = new Cookie("someValue", "value"); - Cookie cookie2 = new Cookie("someValue2", "value2"); - resp.setHeader("SomeNameForHeader", "someValueForHeader"); - resp.addCookie(cookie1); - resp.addCookie(cookie2); - super.sendTextResponse(200, "200", resp); - } - - }); - - HashMap> responseHeaders = new HashMap<>(); - - { - String response = HttpRequest.sendGETRequestWithResponseHeaders(process.getProcess(), "", - "http://localhost:3567/getTestWithHeaders", null, 1000, 1000, null, responseHeaders, true); - assertEquals(response, "200"); - assertTrue(responseHeaders.containsKey("SomeNameForHeader")); - assertEquals(responseHeaders.get("SomeNameForHeader"), Collections.singletonList("someValueForHeader")); - assertTrue(responseHeaders.containsKey("Set-Cookie")); - assertEquals(2, responseHeaders.get("Set-Cookie").size()); - assertTrue(responseHeaders.get("Set-Cookie").contains("someValue=value")); - assertTrue(responseHeaders.get("Set-Cookie").contains("someValue2=value2")); - } - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } } diff --git a/src/test/java/io/supertokens/test/JWKSPublicAPITest.java b/src/test/java/io/supertokens/test/JWKSPublicAPITest.java index a62c7c4f7..4258698d1 100644 --- a/src/test/java/io/supertokens/test/JWKSPublicAPITest.java +++ b/src/test/java/io/supertokens/test/JWKSPublicAPITest.java @@ -16,23 +16,26 @@ package io.supertokens.test; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import io.supertokens.ProcessState; -import io.supertokens.httpRequest.HttpRequest; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import org.junit.AfterClass; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; -import static org.junit.Assert.*; +import io.supertokens.ProcessState; +import io.supertokens.httpRequest.HttpRequest; public class JWKSPublicAPITest { @Rule @@ -80,10 +83,10 @@ public void testCacheControlValue() throws Exception { assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); // check regular output - Map> responseHeaders = new HashMap<>(); + Map responseHeaders = new HashMap<>(); JsonObject response = HttpRequest.sendGETRequestWithResponseHeaders(process.getProcess(), "", "http://localhost:3567/.well-known/jwks.json", null, - 1000, 1000, null, responseHeaders, true); + 1000, 1000, null, responseHeaders); assertEquals(response.entrySet().size(), 1); @@ -91,14 +94,14 @@ public void testCacheControlValue() throws Exception { JsonArray keys = response.get("keys").getAsJsonArray(); assertEquals(keys.size(), 2); - long maxAge = getMaxAgeValue(responseHeaders.get("Cache-Control").get(0)); + long maxAge = getMaxAgeValue(responseHeaders.get("Cache-Control")); assertTrue(maxAge >= 3538 && maxAge <= 3540); Thread.sleep(2000); response = HttpRequest.sendGETRequestWithResponseHeaders(process.getProcess(), "", "http://localhost:3567/.well-known/jwks.json", null, - 1000, 1000, null, responseHeaders, true); + 1000, 1000, null, responseHeaders); assertEquals(response.entrySet().size(), 1); @@ -106,7 +109,7 @@ public void testCacheControlValue() throws Exception { keys = response.get("keys").getAsJsonArray(); assertEquals(keys.size(), 2); - long newMaxAge = getMaxAgeValue(responseHeaders.get("Cache-Control").get(0)); + long newMaxAge = getMaxAgeValue(responseHeaders.get("Cache-Control")); assertTrue(maxAge - newMaxAge >= 2 && maxAge - newMaxAge <= 3); process.kill(); diff --git a/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java b/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java index 424574468..bc27456e3 100644 --- a/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java +++ b/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java @@ -20,14 +20,13 @@ import io.supertokens.ProcessState; import io.supertokens.httpRequest.HttpResponseException; import io.supertokens.oauth.OAuth; -import io.supertokens.oauth.exceptions.OAuthAuthException; +import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.pluginInterface.RECIPE_ID; 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.oauth.OAuthAuthResponse; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; @@ -41,250 +40,250 @@ import static org.junit.Assert.*; public class OAuthAuthAPITest { - TestingProcessManager.TestingProcess process; - - @Rule - public TestRule watchman = Utils.getOnFailure(); - - @AfterClass - public static void afterTesting() { - Utils.afterTesting(); - } - - @Before - public void beforeEach() throws InterruptedException { - Utils.reset(); - } - - - @Test - public void testLocalhostChangedToApiDomain() - throws StorageQueryException, OAuthAuthException, HttpResponseException, TenantOrAppNotFoundException, - InvalidConfigException, IOException, OAuth2ClientAlreadyExistsForAppException, - io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { - - String[] args = {"../"}; - - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; - String redirectUri = "http://localhost.com:3031/auth/callback/ory"; - String responseType = "code"; - String scope = "profile"; - String state = "%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BDv%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD"; - - OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); - - AppIdentifier testApp = new AppIdentifier("", ""); - oAuthStorage.addClientForApp(testApp, clientId); - - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("clientId", clientId); - requestBody.addProperty("redirectUri", redirectUri); - requestBody.addProperty("responseType", responseType); - requestBody.addProperty("scope", scope); - requestBody.addProperty("state", state); - - OAuthAuthResponse response = OAuth.getAuthorizationUrl(process.getProcess(), new AppIdentifier("", ""), - oAuthStorage, requestBody); - - assertNotNull(response); - assertNotNull(response.redirectTo); - assertNotNull(response.cookies); - - assertTrue(response.redirectTo.startsWith("{apiDomain}/login?login_challenge=")); - assertTrue(response.cookies.get(0).startsWith("ory_hydra_login_csrf_dev_134972871=")); - - - - { - JsonObject actualResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", - "http://localhost:3567/recipe/oauth/auth", requestBody, 1000, 1000, null, - null, RECIPE_ID.OAUTH.toString()); - - assertEquals("OK", actualResponse.get("status").getAsString()); - assertTrue(actualResponse.has("redirectTo")); - assertTrue(actualResponse.has("cookies")); - assertTrue(actualResponse.get("redirectTo").getAsString().startsWith("{apiDomain}/login?login_challenge=")); - assertEquals(1, actualResponse.getAsJsonArray("cookies").size()); - assertTrue(actualResponse.getAsJsonArray("cookies").get(0).getAsString().startsWith("ory_hydra_login_csrf_dev_134972871=")); - } - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - - @Test - public void testCalledWithWrongClientIdNotInST_exceptionThrown() - throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException, IOException, - io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { - - - String[] args = {"../"}; - - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientId = "Not-Existing-In-Client-App-Table"; - String redirectUri = "http://localhost.com:3031/auth/callback/ory"; - String responseType = "code"; - String scope = "profile"; - String state = "%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BDv%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD"; - - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("clientId", clientId); - requestBody.addProperty("redirectUri", redirectUri); - requestBody.addProperty("responseType", responseType); - requestBody.addProperty("scope", scope); - requestBody.addProperty("state", state); - - OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); - - AppIdentifier testApp = new AppIdentifier("", ""); - oAuthStorage.addClientForApp(testApp, clientId); - - OAuthAuthException thrown = assertThrows(OAuthAuthException.class, () -> { - - OAuthAuthResponse response = OAuth.getAuthorizationUrl(process.getProcess(), new AppIdentifier("", ""), - oAuthStorage, requestBody); - }); - - String expectedError = "invalid_client"; - String expectedDescription = "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The requested OAuth 2.0 Client does not exist."; - - assertEquals(expectedError, thrown.error); - assertEquals(expectedDescription, thrown.errorDescription); - - { - JsonObject actualResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", - "http://localhost:3567/recipe/oauth/auth", requestBody, 1000, 1000, null, - null, RECIPE_ID.OAUTH.toString()); - - assertEquals("OAUTH2_AUTH_ERROR", actualResponse.get("status").getAsString()); - assertTrue(actualResponse.has("error")); - assertTrue(actualResponse.has("errorDescription")); - assertEquals(expectedError,actualResponse.get("error").getAsString()); - assertEquals(expectedDescription, actualResponse.get("errorDescription").getAsString()); - } - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - - @Test - public void testCalledWithWrongClientIdNotInHydraButInST_exceptionThrown() - throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException, - io.supertokens.test.httpRequest.HttpResponseException, IOException, InterruptedException { - - - String[] args = {"../"}; - - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679NotInHydra"; - String redirectUri = "http://localhost.com:3031/auth/callback/ory"; - String responseType = "code"; - String scope = "profile"; - String state = "%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BDv%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD"; - - OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); - - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("clientId", clientId); - requestBody.addProperty("redirectUri", redirectUri); - requestBody.addProperty("responseType", responseType); - requestBody.addProperty("scope", scope); - requestBody.addProperty("state", state); - - AppIdentifier testApp = new AppIdentifier("", ""); - oAuthStorage.addClientForApp(testApp, clientId); - - OAuthAuthException thrown = assertThrows(OAuthAuthException.class, () -> { - - OAuthAuthResponse response = OAuth.getAuthorizationUrl(process.getProcess(), new AppIdentifier("", ""), - oAuthStorage, requestBody); - }); - - String expectedError = "invalid_client"; - String expectedDescription = "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The requested OAuth 2.0 Client does not exist."; - - assertEquals(expectedError, thrown.error); - assertEquals(expectedDescription, thrown.errorDescription); - - { - JsonObject actualResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", - "http://localhost:3567/recipe/oauth/auth", requestBody, 1000, 1000, null, - null, RECIPE_ID.OAUTH.toString()); - - assertEquals("OAUTH2_AUTH_ERROR", actualResponse.get("status").getAsString()); - assertTrue(actualResponse.has("error")); - assertTrue(actualResponse.has("errorDescription")); - assertEquals(expectedError,actualResponse.get("error").getAsString()); - assertEquals(expectedDescription, actualResponse.get("errorDescription").getAsString()); - - } - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - - @Test - public void testCalledWithWrongRedirectUrl_exceptionThrown() - throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException, - io.supertokens.test.httpRequest.HttpResponseException, IOException, InterruptedException { - - - String[] args = {"../"}; - - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; - String redirectUri = "http://localhost.com:3031/auth/callback/ory_not_the_registered_one"; - String responseType = "code"; - String scope = "profile"; - String state = "%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BDv%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD"; - - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("clientId", clientId); - requestBody.addProperty("redirectUri", redirectUri); - requestBody.addProperty("responseType", responseType); - requestBody.addProperty("scope", scope); - requestBody.addProperty("state", state); - - OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); - - AppIdentifier testApp = new AppIdentifier("", ""); - oAuthStorage.addClientForApp(testApp, clientId); - - OAuthAuthException thrown = assertThrows(OAuthAuthException.class, () -> { - - OAuthAuthResponse response = OAuth.getAuthorizationUrl(process.getProcess(), new AppIdentifier("", ""), - oAuthStorage, requestBody); - }); - - String expectedError = "invalid_request"; - String expectedDescription = "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. The 'redirect_uri' parameter does not match any of the OAuth 2.0 Client's pre-registered redirect urls."; - - assertEquals(expectedError, thrown.error); - assertEquals(expectedDescription, thrown.errorDescription); - - { - - JsonObject actualResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", - "http://localhost:3567/recipe/oauth/auth", requestBody, 1000, 1000, null, - null, RECIPE_ID.OAUTH.toString()); - - assertEquals("OAUTH2_AUTH_ERROR", actualResponse.get("status").getAsString()); - assertTrue(actualResponse.has("error")); - assertTrue(actualResponse.has("errorDescription")); - assertEquals(expectedError, actualResponse.get("error").getAsString()); - assertEquals(expectedDescription, actualResponse.get("errorDescription").getAsString()); - - } - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } +// TestingProcessManager.TestingProcess process; +// +// @Rule +// public TestRule watchman = Utils.getOnFailure(); +// +// @AfterClass +// public static void afterTesting() { +// Utils.afterTesting(); +// } +// +// @Before +// public void beforeEach() throws InterruptedException { +// Utils.reset(); +// } +// +// +// @Test +// public void testLocalhostChangedToApiDomain() +// throws StorageQueryException, OAuthAPIException, HttpResponseException, TenantOrAppNotFoundException, +// InvalidConfigException, IOException, OAuth2ClientAlreadyExistsForAppException, +// io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { +// +// String[] args = {"../"}; +// +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; +// String redirectUri = "http://localhost.com:3031/auth/callback/ory"; +// String responseType = "code"; +// String scope = "profile"; +// String state = "%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BDv%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD"; +// +// OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); +// +// AppIdentifier testApp = new AppIdentifier("", ""); +// oAuthStorage.addClientForApp(testApp, clientId); +// +// JsonObject requestBody = new JsonObject(); +// requestBody.addProperty("clientId", clientId); +// requestBody.addProperty("redirectUri", redirectUri); +// requestBody.addProperty("responseType", responseType); +// requestBody.addProperty("scope", scope); +// requestBody.addProperty("state", state); +// +// OAuthAuthResponse response = OAuth.getAuthorizationUrl(process.getProcess(), new AppIdentifier("", ""), +// oAuthStorage, requestBody); +// +// assertNotNull(response); +// assertNotNull(response.redirectTo); +// assertNotNull(response.cookies); +// +// assertTrue(response.redirectTo.startsWith("{apiDomain}/login?login_challenge=")); +// assertTrue(response.cookies.get(0).startsWith("ory_hydra_login_csrf_dev_134972871=")); +// +// +// +// { +// JsonObject actualResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", +// "http://localhost:3567/recipe/oauth/auth", requestBody, 1000, 1000, null, +// null, RECIPE_ID.OAUTH.toString()); +// +// assertEquals("OK", actualResponse.get("status").getAsString()); +// assertTrue(actualResponse.has("redirectTo")); +// assertTrue(actualResponse.has("cookies")); +// assertTrue(actualResponse.get("redirectTo").getAsString().startsWith("{apiDomain}/login?login_challenge=")); +// assertEquals(1, actualResponse.getAsJsonArray("cookies").size()); +// assertTrue(actualResponse.getAsJsonArray("cookies").get(0).getAsString().startsWith("ory_hydra_login_csrf_dev_134972871=")); +// } +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void testCalledWithWrongClientIdNotInST_exceptionThrown() +// throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException, IOException, +// io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { +// +// +// String[] args = {"../"}; +// +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientId = "Not-Existing-In-Client-App-Table"; +// String redirectUri = "http://localhost.com:3031/auth/callback/ory"; +// String responseType = "code"; +// String scope = "profile"; +// String state = "%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BDv%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD"; +// +// JsonObject requestBody = new JsonObject(); +// requestBody.addProperty("clientId", clientId); +// requestBody.addProperty("redirectUri", redirectUri); +// requestBody.addProperty("responseType", responseType); +// requestBody.addProperty("scope", scope); +// requestBody.addProperty("state", state); +// +// OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); +// +// AppIdentifier testApp = new AppIdentifier("", ""); +// oAuthStorage.addClientForApp(testApp, clientId); +// +// OAuthAPIException thrown = assertThrows(OAuthAPIException.class, () -> { +// +// OAuthAuthResponse response = OAuth.getAuthorizationUrl(process.getProcess(), new AppIdentifier("", ""), +// oAuthStorage, requestBody); +// }); +// +// String expectedError = "invalid_client"; +// String expectedDescription = "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The requested OAuth 2.0 Client does not exist."; +// +// assertEquals(expectedError, thrown.error); +// assertEquals(expectedDescription, thrown.errorDescription); +// +// { +// JsonObject actualResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", +// "http://localhost:3567/recipe/oauth/auth", requestBody, 1000, 1000, null, +// null, RECIPE_ID.OAUTH.toString()); +// +// assertEquals("OAUTH2_AUTH_ERROR", actualResponse.get("status").getAsString()); +// assertTrue(actualResponse.has("error")); +// assertTrue(actualResponse.has("errorDescription")); +// assertEquals(expectedError,actualResponse.get("error").getAsString()); +// assertEquals(expectedDescription, actualResponse.get("errorDescription").getAsString()); +// } +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void testCalledWithWrongClientIdNotInHydraButInST_exceptionThrown() +// throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException, +// io.supertokens.test.httpRequest.HttpResponseException, IOException, InterruptedException { +// +// +// String[] args = {"../"}; +// +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679NotInHydra"; +// String redirectUri = "http://localhost.com:3031/auth/callback/ory"; +// String responseType = "code"; +// String scope = "profile"; +// String state = "%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BDv%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD"; +// +// OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); +// +// JsonObject requestBody = new JsonObject(); +// requestBody.addProperty("clientId", clientId); +// requestBody.addProperty("redirectUri", redirectUri); +// requestBody.addProperty("responseType", responseType); +// requestBody.addProperty("scope", scope); +// requestBody.addProperty("state", state); +// +// AppIdentifier testApp = new AppIdentifier("", ""); +// oAuthStorage.addClientForApp(testApp, clientId); +// +// OAuthAPIException thrown = assertThrows(OAuthAPIException.class, () -> { +// +// OAuthAuthResponse response = OAuth.getAuthorizationUrl(process.getProcess(), new AppIdentifier("", ""), +// oAuthStorage, requestBody); +// }); +// +// String expectedError = "invalid_client"; +// String expectedDescription = "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The requested OAuth 2.0 Client does not exist."; +// +// assertEquals(expectedError, thrown.error); +// assertEquals(expectedDescription, thrown.errorDescription); +// +// { +// JsonObject actualResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", +// "http://localhost:3567/recipe/oauth/auth", requestBody, 1000, 1000, null, +// null, RECIPE_ID.OAUTH.toString()); +// +// assertEquals("OAUTH2_AUTH_ERROR", actualResponse.get("status").getAsString()); +// assertTrue(actualResponse.has("error")); +// assertTrue(actualResponse.has("errorDescription")); +// assertEquals(expectedError,actualResponse.get("error").getAsString()); +// assertEquals(expectedDescription, actualResponse.get("errorDescription").getAsString()); +// +// } +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void testCalledWithWrongRedirectUrl_exceptionThrown() +// throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException, +// io.supertokens.test.httpRequest.HttpResponseException, IOException, InterruptedException { +// +// +// String[] args = {"../"}; +// +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; +// String redirectUri = "http://localhost.com:3031/auth/callback/ory_not_the_registered_one"; +// String responseType = "code"; +// String scope = "profile"; +// String state = "%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BDv%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD"; +// +// JsonObject requestBody = new JsonObject(); +// requestBody.addProperty("clientId", clientId); +// requestBody.addProperty("redirectUri", redirectUri); +// requestBody.addProperty("responseType", responseType); +// requestBody.addProperty("scope", scope); +// requestBody.addProperty("state", state); +// +// OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); +// +// AppIdentifier testApp = new AppIdentifier("", ""); +// oAuthStorage.addClientForApp(testApp, clientId); +// +// OAuthAPIException thrown = assertThrows(OAuthAPIException.class, () -> { +// +// OAuthAuthResponse response = OAuth.getAuthorizationUrl(process.getProcess(), new AppIdentifier("", ""), +// oAuthStorage, requestBody); +// }); +// +// String expectedError = "invalid_request"; +// String expectedDescription = "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. The 'redirect_uri' parameter does not match any of the OAuth 2.0 Client's pre-registered redirect urls."; +// +// assertEquals(expectedError, thrown.error); +// assertEquals(expectedDescription, thrown.errorDescription); +// +// { +// +// JsonObject actualResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", +// "http://localhost:3567/recipe/oauth/auth", requestBody, 1000, 1000, null, +// null, RECIPE_ID.OAUTH.toString()); +// +// assertEquals("OAUTH2_AUTH_ERROR", actualResponse.get("status").getAsString()); +// assertTrue(actualResponse.has("error")); +// assertTrue(actualResponse.has("errorDescription")); +// assertEquals(expectedError, actualResponse.get("error").getAsString()); +// assertEquals(expectedDescription, actualResponse.get("errorDescription").getAsString()); +// +// } +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } } diff --git a/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java b/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java index a4a2c067b..8b1169853 100644 --- a/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java +++ b/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java @@ -24,7 +24,6 @@ import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; @@ -43,430 +42,429 @@ import static org.junit.Assert.assertTrue; public class OAuthClientsAPITest { - TestingProcessManager.TestingProcess process; - - @Rule - public TestRule watchman = Utils.getOnFailure(); - - @AfterClass - public static void afterTesting() { - Utils.afterTesting(); - } - - @Before - public void beforeEach() throws InterruptedException { - Utils.reset(); - } - - @Test - public void testClientRegisteredForApp() - throws HttpResponseException, IOException, InterruptedException, - io.supertokens.httpRequest.HttpResponseException { - - String[] args = {"../"}; - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientName = "jozef"; - String scope = "profile"; - - OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); - - - { - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("clientName", clientName); - requestBody.addProperty("scope", scope); - - JsonArray grantTypes = new JsonArray(); - grantTypes.add(new JsonPrimitive("refresh_token")); - grantTypes.add(new JsonPrimitive("authorization_code")); - requestBody.add("grantTypes", grantTypes); - - JsonArray responseTypes = new JsonArray(); - responseTypes.add(new JsonPrimitive("code")); - responseTypes.add(new JsonPrimitive("id_token")); - requestBody.add("responseTypes", responseTypes); - - JsonObject actualResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", - "http://localhost:3567/recipe/oauth/clients", requestBody, 1000, 1000, null, - null, RECIPE_ID.OAUTH.toString()); - - assertTrue(actualResponse.has("client")); - JsonObject client = actualResponse.get("client").getAsJsonObject(); - - assertTrue(client.has("clientSecret")); - assertTrue(client.has("clientId")); - - String clientId = client.get("clientId").getAsString(); - - Map queryParams = new HashMap<>(); - queryParams.put("clientId", client.get("clientId").getAsString()); - JsonObject loadedClient = HttpRequest.sendGETRequest(process.getProcess(), "", - "http://localhost:3567/recipe/oauth/clients", queryParams,10000,10000, null); - - assertTrue(loadedClient.has("client")); - JsonObject loadedClientJson = loadedClient.get("client").getAsJsonObject(); - assertFalse(loadedClientJson.has("clientSecret")); //this should only be sent when registering - assertEquals(clientId, loadedClientJson.get("clientId").getAsString()); - - {//delete client - JsonObject deleteRequestBody = new JsonObject(); - deleteRequestBody.addProperty("clientId", clientId); - JsonObject deleteResponse = HttpRequestForTesting.sendJsonDELETERequest(process.getProcess(), "", - "http://localhost:3567/recipe/oauth/clients", deleteRequestBody, 1000, 1000, null, - null, RECIPE_ID.OAUTH.toString()); - - assertTrue(deleteResponse.isJsonObject()); - assertEquals("OK", deleteResponse.get("status").getAsString()); //empty response - - } - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - } - - @Test - public void testMissingRequiredField_throwsException() throws InterruptedException { - - String[] args = {"../"}; - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientName = "jozef"; - //notice missing 'scope' field! - - { - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("clientName", clientName); - //notice missing 'scope' field - - JsonArray grantTypes = new JsonArray(); - grantTypes.add(new JsonPrimitive("refresh_token")); - grantTypes.add(new JsonPrimitive("authorization_code")); - requestBody.add("grantTypes", grantTypes); - - JsonArray responseTypes = new JsonArray(); - responseTypes.add(new JsonPrimitive("code")); - responseTypes.add(new JsonPrimitive("id_token")); - requestBody.add("responseTypes", responseTypes); - - io.supertokens.test.httpRequest.HttpResponseException expected = assertThrows(io.supertokens.test.httpRequest.HttpResponseException.class, () -> { - HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", - "http://localhost:3567/recipe/oauth/clients", requestBody, 1000, 1000, null, - null, RECIPE_ID.OAUTH.toString()); - }); - - assertEquals(400, expected.statusCode); - assertEquals("Http error. Status Code: 400. Message: Field name `scope` is missing in JSON input", expected.getMessage()); - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - } - - @Test - public void testMoreFieldAreIgnored() - throws InterruptedException, HttpResponseException, IOException { - - String[] args = {"../"}; - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientName = "jozef"; - String scope = "scope"; - String maliciousAttempt = "giveMeAllYourBelongings!"; //here! - - { - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("clientName", clientName); - requestBody.addProperty("scope", scope); - requestBody.addProperty("dontMindMe", maliciousAttempt); //here! - - JsonArray grantTypes = new JsonArray(); - grantTypes.add(new JsonPrimitive("refresh_token")); - grantTypes.add(new JsonPrimitive("authorization_code")); - requestBody.add("grantTypes", grantTypes); - - JsonArray responseTypes = new JsonArray(); - responseTypes.add(new JsonPrimitive("code")); - responseTypes.add(new JsonPrimitive("id_token")); - requestBody.add("responseTypes", responseTypes); - - JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", - "http://localhost:3567/recipe/oauth/clients", requestBody, 1000, 1000, null, - null, RECIPE_ID.OAUTH.toString()); - - assertEquals("OK", response.get("status").getAsString()); - assertTrue(response.has("client")); - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - } - - - @Test - public void testGETClientNotExisting_returnsError() - throws InterruptedException, io.supertokens.httpRequest.HttpResponseException, - IOException { - - String[] args = {"../"}; - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientId = "not-an-existing-one"; - - Map queryParams = new HashMap<>(); - queryParams.put("clientId", clientId); - JsonObject response = HttpRequest.sendGETRequest(process.getProcess(), "", - "http://localhost:3567/recipe/oauth/clients", queryParams, 10000, 10000, null); - - assertEquals("OAUTH2_CLIENT_NOT_FOUND_ERROR", response.get("status").getAsString()); - assertEquals("Unable to locate the resource", response.get("error").getAsString()); - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - - @Test - public void testClientUpdatePatch() - throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, - io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { - - String[] args = {"../"}; - - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; - String propToChangeKey = "clientName"; - String newValue = "Jozef"; - - OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); - - AppIdentifier testApp = new AppIdentifier("", ""); - oAuthStorage.addClientForApp(testApp, clientId); - - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("clientId", clientId); - requestBody.addProperty(propToChangeKey, newValue); - - JsonObject actualResponse = HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), - "http://localhost:3567/recipe/oauth/clients", requestBody); - - assertEquals("OK", actualResponse.get("status").getAsString()); - assertTrue(actualResponse.has("client")); - - JsonObject updatedClient = actualResponse.get("client").getAsJsonObject(); - - assertTrue(updatedClient.has(propToChangeKey)); - assertEquals(newValue, updatedClient.get(propToChangeKey).getAsString()); - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - - @Test - public void testClientUpdatePatch_multipleFields() - throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, - io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { - - String[] args = {"../"}; - - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; - String propToChangeKey = "clientName"; - String newValue = "Jozef2"; - - String listPropToChange = "grantTypes"; - JsonArray newListValue = new JsonArray(); - newListValue.add(new JsonPrimitive("test1")); - newListValue.add(new JsonPrimitive("test2")); - - OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); - - AppIdentifier testApp = new AppIdentifier("", ""); - oAuthStorage.addClientForApp(testApp, clientId); - - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("clientId", clientId); - requestBody.addProperty(propToChangeKey, newValue); - requestBody.add(listPropToChange, newListValue); - - JsonObject actualResponse = HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), - "http://localhost:3567/recipe/oauth/clients", requestBody); - - assertEquals("OK", actualResponse.get("status").getAsString()); - assertTrue(actualResponse.has("client")); - - JsonObject updatedClient = actualResponse.get("client").getAsJsonObject(); - - assertTrue(updatedClient.has(propToChangeKey)); - assertEquals(newValue, updatedClient.get(propToChangeKey).getAsString()); - - assertTrue(updatedClient.has(listPropToChange)); - assertEquals(newListValue.getAsJsonArray(), updatedClient.get(listPropToChange).getAsJsonArray()); - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - - @Test - public void testClientUpdatePatch_missingClientIdResultsInError() - throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, - io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { - - String[] args = {"../"}; - - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; - String propToChangeKey = "clientName"; - String newValue = "Jozef2"; - - String listPropToChange = "grantTypes"; - JsonArray newListValue = new JsonArray(); - newListValue.add(new JsonPrimitive("test1")); - newListValue.add(new JsonPrimitive("test2")); - - OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); - - AppIdentifier testApp = new AppIdentifier("", ""); - oAuthStorage.addClientForApp(testApp, clientId); - - JsonObject requestBody = new JsonObject(); - //note the missing client Id - requestBody.addProperty(propToChangeKey, newValue); - requestBody.add(listPropToChange, newListValue); - - io.supertokens.test.httpRequest.HttpResponseException expected = assertThrows(io.supertokens.test.httpRequest.HttpResponseException.class, - () -> { HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), - "http://localhost:3567/recipe/oauth/clients", requestBody); - }); - - assertEquals(400, expected.statusCode); - assertEquals("Http error. Status Code: 400. Message: Field name `clientId` is missing in JSON input\n", expected.getMessage()); - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - - @Test - public void testClientUpdatePatch_hydraErrortResultsInError() - throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, - io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { - - String[] args = {"../"}; - - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; - String propToChangeKey = "clientName"; - String newValue = "Jozef2"; - - String notAlistPropToChange = "scope"; - JsonArray newListValue = new JsonArray(); - newListValue.add(new JsonPrimitive("test1")); - newListValue.add(new JsonPrimitive("test2")); - - OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); - - AppIdentifier testApp = new AppIdentifier("", ""); - oAuthStorage.addClientForApp(testApp, clientId); - - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("clientId", clientId); - requestBody.addProperty(propToChangeKey, newValue); - requestBody.add(notAlistPropToChange, newListValue); - - HttpResponseException expected = assertThrows(HttpResponseException.class, () -> { - HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), - "http://localhost:3567/recipe/oauth/clients", requestBody); - }); - - assertEquals("Http error. Status Code: 500. Message: Internal Error\n", expected.getMessage()); - assertEquals(500, expected.statusCode); - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - - @Test - public void testClientUpdatePatch_invalidInputDataResultsInError() - throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, - io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { - - String[] args = {"../"}; - - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; - String propToChangeKey = "clientName"; - String newValue = "Jozef2"; - - String notAlistPropToChange = "allowed_cors_origins"; - JsonArray newListValue = new JsonArray(); - newListValue.add(new JsonPrimitive("*")); - newListValue.add(new JsonPrimitive("appleTree")); - - OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); - - AppIdentifier testApp = new AppIdentifier("", ""); - oAuthStorage.addClientForApp(testApp, clientId); - - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("clientId", clientId); - requestBody.addProperty(propToChangeKey, newValue); - requestBody.add(notAlistPropToChange, newListValue); - - io.supertokens.test.httpRequest.HttpResponseException expected = assertThrows(io.supertokens.test.httpRequest.HttpResponseException.class, - () -> { HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), - "http://localhost:3567/recipe/oauth/clients", requestBody); - }); - - assertEquals(400, expected.statusCode); - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - - @Test - public void testClientUpdatePatch_notExistingClientResultsInNotFound() - throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, - io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { - - String[] args = {"../"}; - - this.process = TestingProcessManager.start(args); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679-Not_Existing"; - String propToChangeKey = "clientName"; - String newValue = "Jozef2"; - - OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); - - AppIdentifier testApp = new AppIdentifier("", ""); - oAuthStorage.addClientForApp(testApp, clientId); // exists at our end, not exists in hydra - - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("clientId", clientId); - requestBody.addProperty(propToChangeKey, newValue); - - JsonObject response = HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), - "http://localhost:3567/recipe/oauth/clients", requestBody); - - assertEquals("OAUTH2_CLIENT_NOT_FOUND_ERROR", response.get("status").getAsString()); - - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } - - +// TestingProcessManager.TestingProcess process; +// +// @Rule +// public TestRule watchman = Utils.getOnFailure(); +// +// @AfterClass +// public static void afterTesting() { +// Utils.afterTesting(); +// } +// +// @Before +// public void beforeEach() throws InterruptedException { +// Utils.reset(); +// } +// +// @Test +// public void testClientRegisteredForApp() +// throws HttpResponseException, IOException, InterruptedException, +// io.supertokens.httpRequest.HttpResponseException { +// +// String[] args = {"../"}; +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientName = "jozef"; +// String scope = "profile"; +// +// OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); +// +// +// { +// JsonObject requestBody = new JsonObject(); +// requestBody.addProperty("clientName", clientName); +// requestBody.addProperty("scope", scope); +// +// JsonArray grantTypes = new JsonArray(); +// grantTypes.add(new JsonPrimitive("refresh_token")); +// grantTypes.add(new JsonPrimitive("authorization_code")); +// requestBody.add("grantTypes", grantTypes); +// +// JsonArray responseTypes = new JsonArray(); +// responseTypes.add(new JsonPrimitive("code")); +// responseTypes.add(new JsonPrimitive("id_token")); +// requestBody.add("responseTypes", responseTypes); +// +// JsonObject actualResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", +// "http://localhost:3567/recipe/oauth/clients", requestBody, 1000, 1000, null, +// null, RECIPE_ID.OAUTH.toString()); +// +// assertTrue(actualResponse.has("client")); +// JsonObject client = actualResponse.get("client").getAsJsonObject(); +// +// assertTrue(client.has("clientSecret")); +// assertTrue(client.has("clientId")); +// +// String clientId = client.get("clientId").getAsString(); +// +// Map queryParams = new HashMap<>(); +// queryParams.put("clientId", client.get("clientId").getAsString()); +// JsonObject loadedClient = HttpRequest.sendGETRequest(process.getProcess(), "", +// "http://localhost:3567/recipe/oauth/clients", queryParams,10000,10000, null); +// +// assertTrue(loadedClient.has("client")); +// JsonObject loadedClientJson = loadedClient.get("client").getAsJsonObject(); +// assertFalse(loadedClientJson.has("clientSecret")); //this should only be sent when registering +// assertEquals(clientId, loadedClientJson.get("clientId").getAsString()); +// +// {//delete client +// JsonObject deleteRequestBody = new JsonObject(); +// deleteRequestBody.addProperty("clientId", clientId); +// JsonObject deleteResponse = HttpRequestForTesting.sendJsonDELETERequest(process.getProcess(), "", +// "http://localhost:3567/recipe/oauth/clients", deleteRequestBody, 1000, 1000, null, +// null, RECIPE_ID.OAUTH.toString()); +// +// assertTrue(deleteResponse.isJsonObject()); +// assertEquals("OK", deleteResponse.get("status").getAsString()); //empty response +// +// } +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// } +// +// @Test +// public void testMissingRequiredField_throwsException() throws InterruptedException { +// +// String[] args = {"../"}; +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientName = "jozef"; +// //notice missing 'scope' field! +// +// { +// JsonObject requestBody = new JsonObject(); +// requestBody.addProperty("clientName", clientName); +// //notice missing 'scope' field +// +// JsonArray grantTypes = new JsonArray(); +// grantTypes.add(new JsonPrimitive("refresh_token")); +// grantTypes.add(new JsonPrimitive("authorization_code")); +// requestBody.add("grantTypes", grantTypes); +// +// JsonArray responseTypes = new JsonArray(); +// responseTypes.add(new JsonPrimitive("code")); +// responseTypes.add(new JsonPrimitive("id_token")); +// requestBody.add("responseTypes", responseTypes); +// +// io.supertokens.test.httpRequest.HttpResponseException expected = assertThrows(io.supertokens.test.httpRequest.HttpResponseException.class, () -> { +// HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", +// "http://localhost:3567/recipe/oauth/clients", requestBody, 1000, 1000, null, +// null, RECIPE_ID.OAUTH.toString()); +// }); +// +// assertEquals(400, expected.statusCode); +// assertEquals("Http error. Status Code: 400. Message: Field name `scope` is missing in JSON input", expected.getMessage()); +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// } +// +// @Test +// public void testMoreFieldAreIgnored() +// throws InterruptedException, HttpResponseException, IOException { +// +// String[] args = {"../"}; +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientName = "jozef"; +// String scope = "scope"; +// String maliciousAttempt = "giveMeAllYourBelongings!"; //here! +// +// { +// JsonObject requestBody = new JsonObject(); +// requestBody.addProperty("clientName", clientName); +// requestBody.addProperty("scope", scope); +// requestBody.addProperty("dontMindMe", maliciousAttempt); //here! +// +// JsonArray grantTypes = new JsonArray(); +// grantTypes.add(new JsonPrimitive("refresh_token")); +// grantTypes.add(new JsonPrimitive("authorization_code")); +// requestBody.add("grantTypes", grantTypes); +// +// JsonArray responseTypes = new JsonArray(); +// responseTypes.add(new JsonPrimitive("code")); +// responseTypes.add(new JsonPrimitive("id_token")); +// requestBody.add("responseTypes", responseTypes); +// +// JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", +// "http://localhost:3567/recipe/oauth/clients", requestBody, 1000, 1000, null, +// null, RECIPE_ID.OAUTH.toString()); +// +// assertEquals("OK", response.get("status").getAsString()); +// assertTrue(response.has("client")); +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// } +// +// +// @Test +// public void testGETClientNotExisting_returnsError() +// throws InterruptedException, io.supertokens.httpRequest.HttpResponseException, +// IOException { +// +// String[] args = {"../"}; +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientId = "not-an-existing-one"; +// +// Map queryParams = new HashMap<>(); +// queryParams.put("clientId", clientId); +// JsonObject response = HttpRequest.sendGETRequest(process.getProcess(), "", +// "http://localhost:3567/recipe/oauth/clients", queryParams, 10000, 10000, null); +// +// assertEquals("OAUTH2_CLIENT_NOT_FOUND_ERROR", response.get("status").getAsString()); +// assertEquals("Unable to locate the resource", response.get("error").getAsString()); +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void testClientUpdatePatch() +// throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, +// io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { +// +// String[] args = {"../"}; +// +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; +// String propToChangeKey = "clientName"; +// String newValue = "Jozef"; +// +// OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); +// +// AppIdentifier testApp = new AppIdentifier("", ""); +// oAuthStorage.addClientForApp(testApp, clientId); +// +// JsonObject requestBody = new JsonObject(); +// requestBody.addProperty("clientId", clientId); +// requestBody.addProperty(propToChangeKey, newValue); +// +// JsonObject actualResponse = HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), +// "http://localhost:3567/recipe/oauth/clients", requestBody); +// +// assertEquals("OK", actualResponse.get("status").getAsString()); +// assertTrue(actualResponse.has("client")); +// +// JsonObject updatedClient = actualResponse.get("client").getAsJsonObject(); +// +// assertTrue(updatedClient.has(propToChangeKey)); +// assertEquals(newValue, updatedClient.get(propToChangeKey).getAsString()); +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void testClientUpdatePatch_multipleFields() +// throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, +// io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { +// +// String[] args = {"../"}; +// +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; +// String propToChangeKey = "clientName"; +// String newValue = "Jozef2"; +// +// String listPropToChange = "grantTypes"; +// JsonArray newListValue = new JsonArray(); +// newListValue.add(new JsonPrimitive("test1")); +// newListValue.add(new JsonPrimitive("test2")); +// +// OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); +// +// AppIdentifier testApp = new AppIdentifier("", ""); +// oAuthStorage.addClientForApp(testApp, clientId); +// +// JsonObject requestBody = new JsonObject(); +// requestBody.addProperty("clientId", clientId); +// requestBody.addProperty(propToChangeKey, newValue); +// requestBody.add(listPropToChange, newListValue); +// +// JsonObject actualResponse = HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), +// "http://localhost:3567/recipe/oauth/clients", requestBody); +// +// assertEquals("OK", actualResponse.get("status").getAsString()); +// assertTrue(actualResponse.has("client")); +// +// JsonObject updatedClient = actualResponse.get("client").getAsJsonObject(); +// +// assertTrue(updatedClient.has(propToChangeKey)); +// assertEquals(newValue, updatedClient.get(propToChangeKey).getAsString()); +// +// assertTrue(updatedClient.has(listPropToChange)); +// assertEquals(newListValue.getAsJsonArray(), updatedClient.get(listPropToChange).getAsJsonArray()); +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void testClientUpdatePatch_missingClientIdResultsInError() +// throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, +// io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { +// +// String[] args = {"../"}; +// +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; +// String propToChangeKey = "clientName"; +// String newValue = "Jozef2"; +// +// String listPropToChange = "grantTypes"; +// JsonArray newListValue = new JsonArray(); +// newListValue.add(new JsonPrimitive("test1")); +// newListValue.add(new JsonPrimitive("test2")); +// +// OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); +// +// AppIdentifier testApp = new AppIdentifier("", ""); +// oAuthStorage.addClientForApp(testApp, clientId); +// +// JsonObject requestBody = new JsonObject(); +// //note the missing client Id +// requestBody.addProperty(propToChangeKey, newValue); +// requestBody.add(listPropToChange, newListValue); +// +// io.supertokens.test.httpRequest.HttpResponseException expected = assertThrows(io.supertokens.test.httpRequest.HttpResponseException.class, +// () -> { HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), +// "http://localhost:3567/recipe/oauth/clients", requestBody); +// }); +// +// assertEquals(400, expected.statusCode); +// assertEquals("Http error. Status Code: 400. Message: Field name `clientId` is missing in JSON input\n", expected.getMessage()); +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void testClientUpdatePatch_hydraErrortResultsInError() +// throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, +// io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { +// +// String[] args = {"../"}; +// +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; +// String propToChangeKey = "clientName"; +// String newValue = "Jozef2"; +// +// String notAlistPropToChange = "scope"; +// JsonArray newListValue = new JsonArray(); +// newListValue.add(new JsonPrimitive("test1")); +// newListValue.add(new JsonPrimitive("test2")); +// +// OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); +// +// AppIdentifier testApp = new AppIdentifier("", ""); +// oAuthStorage.addClientForApp(testApp, clientId); +// +// JsonObject requestBody = new JsonObject(); +// requestBody.addProperty("clientId", clientId); +// requestBody.addProperty(propToChangeKey, newValue); +// requestBody.add(notAlistPropToChange, newListValue); +// +// HttpResponseException expected = assertThrows(HttpResponseException.class, () -> { +// HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), +// "http://localhost:3567/recipe/oauth/clients", requestBody); +// }); +// +// assertEquals("Http error. Status Code: 500. Message: Internal Error\n", expected.getMessage()); +// assertEquals(500, expected.statusCode); +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void testClientUpdatePatch_invalidInputDataResultsInError() +// throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, +// io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { +// +// String[] args = {"../"}; +// +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; +// String propToChangeKey = "clientName"; +// String newValue = "Jozef2"; +// +// String notAlistPropToChange = "allowed_cors_origins"; +// JsonArray newListValue = new JsonArray(); +// newListValue.add(new JsonPrimitive("*")); +// newListValue.add(new JsonPrimitive("appleTree")); +// +// OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); +// +// AppIdentifier testApp = new AppIdentifier("", ""); +// oAuthStorage.addClientForApp(testApp, clientId); +// +// JsonObject requestBody = new JsonObject(); +// requestBody.addProperty("clientId", clientId); +// requestBody.addProperty(propToChangeKey, newValue); +// requestBody.add(notAlistPropToChange, newListValue); +// +// io.supertokens.test.httpRequest.HttpResponseException expected = assertThrows(io.supertokens.test.httpRequest.HttpResponseException.class, +// () -> { HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), +// "http://localhost:3567/recipe/oauth/clients", requestBody); +// }); +// +// assertEquals(400, expected.statusCode); +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// +// @Test +// public void testClientUpdatePatch_notExistingClientResultsInNotFound() +// throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, +// io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { +// +// String[] args = {"../"}; +// +// this.process = TestingProcessManager.start(args); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); +// +// String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679-Not_Existing"; +// String propToChangeKey = "clientName"; +// String newValue = "Jozef2"; +// +// OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); +// +// AppIdentifier testApp = new AppIdentifier("", ""); +// oAuthStorage.addClientForApp(testApp, clientId); // exists at our end, not exists in hydra +// +// JsonObject requestBody = new JsonObject(); +// requestBody.addProperty("clientId", clientId); +// requestBody.addProperty(propToChangeKey, newValue); +// +// JsonObject response = HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), +// "http://localhost:3567/recipe/oauth/clients", requestBody); +// +// assertEquals("OAUTH2_CLIENT_NOT_FOUND_ERROR", response.get("status").getAsString()); +// +// process.kill(); +// assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); +// } +// }