diff --git a/src/main/java/io/supertokens/Main.java b/src/main/java/io/supertokens/Main.java index 1eef7e500..1bb240977 100644 --- a/src/main/java/io/supertokens/Main.java +++ b/src/main/java/io/supertokens/Main.java @@ -20,7 +20,7 @@ import io.supertokens.config.Config; import io.supertokens.config.CoreConfig; import io.supertokens.cronjobs.Cronjobs; -import io.supertokens.cronjobs.cleanupOAuthRevokeList.CleanupOAuthRevokeList; +import io.supertokens.cronjobs.cleanupOAuthRevokeList.CleanupOAuthRevokeListAndChallenges; import io.supertokens.cronjobs.deleteExpiredAccessTokenSigningKeys.DeleteExpiredAccessTokenSigningKeys; import io.supertokens.cronjobs.deleteExpiredDashboardSessions.DeleteExpiredDashboardSessions; import io.supertokens.cronjobs.deleteExpiredEmailVerificationTokens.DeleteExpiredEmailVerificationTokens; @@ -257,7 +257,7 @@ private void init() throws IOException, StorageQueryException { // starts DeleteExpiredAccessTokenSigningKeys cronjob if the access token signing keys can change Cronjobs.addCronjob(this, DeleteExpiredAccessTokenSigningKeys.init(this, uniqueUserPoolIdsTenants)); - Cronjobs.addCronjob(this, CleanupOAuthRevokeList.init(this, uniqueUserPoolIdsTenants)); + Cronjobs.addCronjob(this, CleanupOAuthRevokeListAndChallenges.init(this, uniqueUserPoolIdsTenants)); // this is to ensure tenantInfos are in sync for the new cron job as well MultitenancyHelper.getInstance(this).refreshCronjobs(); diff --git a/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java b/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeListAndChallenges.java similarity index 75% rename from src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java rename to src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeListAndChallenges.java index a94d65d51..d282b26b8 100644 --- a/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java +++ b/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeListAndChallenges.java @@ -12,19 +12,19 @@ import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.storageLayer.StorageLayer; -public class CleanupOAuthRevokeList extends CronTask { +public class CleanupOAuthRevokeListAndChallenges extends CronTask { public static final String RESOURCE_KEY = "io.supertokens.cronjobs.cleanupOAuthRevokeList" + ".CleanupOAuthRevokeList"; - private CleanupOAuthRevokeList(Main main, List> tenantsInfo) { + private CleanupOAuthRevokeListAndChallenges(Main main, List> tenantsInfo) { super("CleanupOAuthRevokeList", main, tenantsInfo, true); } - public static CleanupOAuthRevokeList init(Main main, List> tenantsInfo) { - return (CleanupOAuthRevokeList) main.getResourceDistributor() + public static CleanupOAuthRevokeListAndChallenges init(Main main, List> tenantsInfo) { + return (CleanupOAuthRevokeListAndChallenges) main.getResourceDistributor() .setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY, - new CleanupOAuthRevokeList(main, tenantsInfo)); + new CleanupOAuthRevokeListAndChallenges(main, tenantsInfo)); } @Override @@ -32,6 +32,7 @@ protected void doTaskPerApp(AppIdentifier app) throws Exception { Storage storage = StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); oauthStorage.cleanUpExpiredAndRevokedTokens(app); + oauthStorage.deleteLogoutChallengesBefore(app, System.currentTimeMillis() - 1000 * 60 * 60 * 48); } @Override diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 288e6593e..4a27e3b84 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -55,6 +55,7 @@ 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.OAuthLogoutChallenge; import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; @@ -3077,6 +3078,43 @@ public void addM2MToken(AppIdentifier appIdentifier, String clientId, long iat, } } + @Override + public void addLogoutChallenge(AppIdentifier appIdentifier, String challenge, String clientId, + String postLogoutRedirectionUri, String state, long timeCreated) throws StorageQueryException { + try { + OAuthQueries.addLogoutChallenge(this, appIdentifier, challenge, clientId, postLogoutRedirectionUri, state, timeCreated); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public OAuthLogoutChallenge getLogoutChallenge(AppIdentifier appIdentifier, String challenge) throws StorageQueryException { + try { + return OAuthQueries.getLogoutChallenge(this, appIdentifier, challenge); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void deleteLogoutChallenge(AppIdentifier appIdentifier, String challenge) throws StorageQueryException { + try { + OAuthQueries.deleteLogoutChallenge(this, appIdentifier, challenge); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void deleteLogoutChallengesBefore(AppIdentifier appIdentifier, long time) throws StorageQueryException { + try { + OAuthQueries.deleteLogoutChallengesBefore(this, appIdentifier, time); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public void cleanUpExpiredAndRevokedTokens(AppIdentifier appIdentifier) throws StorageQueryException { try { diff --git a/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java b/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java index c14646456..75b98b3d5 100644 --- a/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java +++ b/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java @@ -176,4 +176,8 @@ public String getOAuthRevokeTable() { public String getOAuthM2MTokensTable() { return "oauth_m2m_tokens"; } + + public String getOAuthLogoutChallengesTable() { + return "oauth_logout_challenges"; + } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java index 13d4ee092..1c23d487d 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java @@ -444,8 +444,15 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc update(start, OAuthQueries.getQueryToCreateOAuthM2MTokenIatIndex(start), NO_OP_SETTER); update(start, OAuthQueries.getQueryToCreateOAuthM2MTokenExpIndex(start), NO_OP_SETTER); } - } + if (!doesTableExists(start, Config.getConfig(start).getOAuthLogoutChallengesTable())) { + getInstance(main).addState(CREATING_NEW_TABLE, null); + update(start, OAuthQueries.getQueryToCreateOAuthLogoutChallengesTable(start), NO_OP_SETTER); + + // index + update(start, OAuthQueries.getQueryToCreateOAuthLogoutChallengesTimeCreatedIndex(start), NO_OP_SETTER); + } + } public static void setKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String key, KeyValueInfo info) diff --git a/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java index 5a27ee8d5..fdac29760 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java @@ -20,6 +20,7 @@ import io.supertokens.inmemorydb.config.Config; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.oauth.OAuthLogoutChallenge; import java.sql.ResultSet; import java.sql.SQLException; @@ -92,6 +93,32 @@ public static String getQueryToCreateOAuthM2MTokenExpIndex(Start start) { + oAuth2M2MTokensTable + "(exp DESC, app_id DESC);"; } + public static String getQueryToCreateOAuthLogoutChallengesTable(Start start) { + String oAuth2LogoutChallengesTable = Config.getConfig(start).getOAuthLogoutChallengesTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + oAuth2LogoutChallengesTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "challenge VARCHAR(128) NOT NULL," + + "client_id VARCHAR(128) NOT NULL," + + "post_logout_redirect_uri VARCHAR(1024)," + + "gid VARCHAR(128)," + + "state VARCHAR(128)," + + "time_created BIGINT NOT NULL," + + "PRIMARY KEY (app_id, challenge)," + + "FOREIGN KEY(app_id, client_id)" + + " REFERENCES " + Config.getConfig(start).getOAuthClientsTable() + "(app_id, client_id) ON DELETE CASCADE," + + "FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + public static String getQueryToCreateOAuthLogoutChallengesTimeCreatedIndex(Start start) { + String oAuth2LogoutChallengesTable = Config.getConfig(start).getOAuthLogoutChallengesTable(); + return "CREATE INDEX IF NOT EXISTS oauth_logout_challenges_time_created_index ON " + + oAuth2LogoutChallengesTable + "(time_created ASC, app_id ASC);"; + } + public static boolean isClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthClientsTable() + @@ -285,4 +312,59 @@ public static void cleanUpExpiredAndRevokedTokens(Start start, AppIdentifier app }); } } + + public static void addLogoutChallenge(Start start, AppIdentifier appIdentifier, String challenge, String clientId, + String postLogoutRedirectionUri, String state, long timeCreated) throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getOAuthLogoutChallengesTable() + + " (app_id, challenge, client_id, post_logout_redirect_uri, state, time_created) VALUES (?, ?, ?, ?, ?, ?)"; + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, challenge); + pst.setString(3, clientId); + pst.setString(4, postLogoutRedirectionUri); + pst.setString(5, state); + pst.setLong(6, timeCreated); + }); + } + + public static OAuthLogoutChallenge getLogoutChallenge(Start start, AppIdentifier appIdentifier, String challenge) throws SQLException, StorageQueryException { + String QUERY = "SELECT challenge, client_id, post_logout_redirect_uri, gid, state, time_created FROM " + + Config.getConfig(start).getOAuthLogoutChallengesTable() + + " WHERE app_id = ? AND challenge = ?"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, challenge); + }, result -> { + if (result.next()) { + return new OAuthLogoutChallenge( + result.getString("challenge"), + result.getString("client_id"), + result.getString("post_logout_redirect_uri"), + result.getString("gid"), + result.getString("state"), + result.getLong("time_created") + ); + } + return null; + }); + } + + public static void deleteLogoutChallenge(Start start, AppIdentifier appIdentifier, String challenge) throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthLogoutChallengesTable() + + " WHERE app_id = ? AND challenge = ?"; + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, challenge); + }); + } + + public static void deleteLogoutChallengesBefore(Start start, AppIdentifier appIdentifier, long time) throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthLogoutChallengesTable() + + " WHERE app_id = ? AND time_created < ?"; + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, time); + }); + } } diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 0c73d6a33..96e6a8484 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -36,6 +36,7 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.oauth.OAuthLogoutChallenge; import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.session.jwt.JWT.JWTException; import io.supertokens.utils.Utils; @@ -330,6 +331,8 @@ public static String transformTokensInAuthRedirect(Main main, AppIdentifier appI public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject jsonBody, String iss, JsonObject accessTokenUpdate, JsonObject idTokenUpdate, boolean useDynamicKey) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { String atHash = null; + System.out.println("transformTokens: " + jsonBody.toString()); + if (jsonBody.has("refresh_token")) { String refreshToken = jsonBody.get("refresh_token").getAsString(); refreshToken = refreshToken.replace("ory_rt_", "st_rt_"); @@ -551,45 +554,58 @@ public static void revokeSessionHandle(Main main, AppIdentifier appIdentifier, S 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 { + public static JsonObject verifyIdTokenAndGetPayload(Main main, AppIdentifier appIdentifier, Storage storage, + String idToken) throws StorageQueryException, OAuthAPIException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + try { + return OAuthToken.getPayloadFromJWTToken(appIdentifier, main, idToken); + } 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); + } + } - String idTokenHint = queryParams.get("idTokenHint"); - String clientId = queryParams.get("clientId"); + 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()); + } - JsonObject idTokenPayload = null; - if (idTokenHint != null) { - queryParams.remove("idTokenHint"); + public static String createLogoutRequestAndReturnRedirectUri(Main main, AppIdentifier appIdentifier, Storage storage, String clientId, + String postLogoutRedirectionUri, String state, String idTokenHint) throws StorageQueryException { + + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - 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); - } - } + String logoutChallenge = UUID.randomUUID().toString(); + oauthStorage.addLogoutChallenge(appIdentifier, logoutChallenge, clientId, postLogoutRedirectionUri, state, System.currentTimeMillis()); + return "{apiDomain}/oauth/logout?logout_challenge=" + logoutChallenge; + } - 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); - } + public static String consumeLogoutChallengeAndGetRedirectUri(Main main, AppIdentifier appIdentifier, Storage storage, String challenge) throws StorageQueryException, OAuthAPIException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + OAuthLogoutChallenge logoutChallenge = oauthStorage.getLogoutChallenge(appIdentifier, challenge); - String clientIdInIdTokenPayload = idTokenPayload.get("aud").getAsString(); + if (logoutChallenge == null) { + throw new OAuthAPIException("invalid_request", "Logout request not found", 400); + } - 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); - } - } + oauthStorage.revoke(appIdentifier, "gid", logoutChallenge.gid, 3600 * 24 * (183 + 31)); + + String url = null; + if (logoutChallenge.postLogoutRedirectionUri != null) { + url = logoutChallenge.postLogoutRedirectionUri; + } else { + url = "{apiDomain}/fallbacks/logout/callback"; + } - queryParams.put("clientId", clientIdInIdTokenPayload); + if (logoutChallenge.state != null) { + return url + "?state=" + logoutChallenge.state; + } else { + return url; } } - public static void addM2MToken(Main main, AppIdentifier appIdentifier, Storage storage, String accessToken) throws StorageQueryException, TenantOrAppNotFoundException, TryRefreshTokenException, UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + public static void deleteLogoutChallenge(Main main, AppIdentifier appIdentifier, Storage storage, String challenge) throws StorageQueryException { 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()); + oauthStorage.deleteLogoutChallenge(appIdentifier, challenge); } } diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index f283ca8b2..44e6a1339 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -280,7 +280,6 @@ private void setupRoutes() { 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)); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java index 7d07c254d..052c47465 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java @@ -8,7 +8,10 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; @@ -30,26 +33,19 @@ public String getPath() { @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String challenge = InputParser.parseStringOrThrowError(input, "challenge", false); 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) { + String redirectTo = OAuth.consumeLogoutChallengeAndGetRedirectUri(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), challenge); + + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + response.addProperty("redirectTo", redirectTo); + super.sendJsonResponse(200, response, resp); + + } catch (OAuthAPIException e) { + OAuthProxyHelper.handleOAuthAPIException(resp, e); + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException 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 deleted file mode 100644 index 143e505b0..000000000 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java +++ /dev/null @@ -1,54 +0,0 @@ -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 index 8e6626733..2fd26071e 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java @@ -4,6 +4,7 @@ import java.util.HashMap; import java.util.Map; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.supertokens.Main; @@ -18,6 +19,7 @@ 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; @@ -35,35 +37,101 @@ public String getPath() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + String clientId = InputParser.getQueryParamOrThrowError(req, "clientId", true); + String idTokenHint = InputParser.getQueryParamOrThrowError(req, "idTokenHint", true); + String postLogoutRedirectionUri = InputParser.getQueryParamOrThrowError(req, "postLogoutRedirectionUri", true); + String state = InputParser.getQueryParamOrThrowError(req, "state", true); + 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); + // Validations + if (idTokenHint == null) { + if (postLogoutRedirectionUri != null ) { + 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. Logout failed because query parameter postLogoutRedirectionUri is set but idTokenHint is missing.", 400); + } + + if (state != null) { + 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. Logout failed because query parameter state is set but idTokenHint is missing.", 400); + } + } + // Verify id token and client id associations + JsonObject idTokenPayload = null; + if (idTokenHint != null) { + idTokenPayload = OAuth.verifyIdTokenAndGetPayload(main, appIdentifier, storage, idTokenHint); + + if (clientId != null) { + String clientIdInIdTokenPayload = idTokenPayload.get("aud").getAsString(); + 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); + } + } else { + clientId = idTokenPayload.get("aud").getAsString(); + } + } + + // Check if the post logout redirection URI is valid for the clientId + if (postLogoutRedirectionUri != null) { + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( + main, req, resp, + appIdentifier, storage, + clientId, // clientIdToCheck + "/admin/clients/" + clientId, // path + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + new HashMap<>(), // queryParams + new HashMap<>() // headers + ); + + if (response == null) { + return; // proxy would have already responded + } + + String[] postLogoutRedirectUris = null; + if (response.jsonResponse.getAsJsonObject().has("postLogoutRedirectUris") && response.jsonResponse.getAsJsonObject().get("postLogoutRedirectUris").isJsonArray()) { + JsonArray postLogoutRedirectUrisArray = response.jsonResponse.getAsJsonObject().get("postLogoutRedirectUris").getAsJsonArray(); + postLogoutRedirectUris = new String[postLogoutRedirectUrisArray.size()]; + for (int i = 0; i < postLogoutRedirectUrisArray.size(); i++) { + postLogoutRedirectUris[i] = postLogoutRedirectUrisArray.get(i).getAsString(); + } + } + + if (postLogoutRedirectUris == null || postLogoutRedirectUris.length == 0) { + throw new OAuthAPIException("", "", 400); + } + + boolean isValidPostLogoutRedirectUri = false; + for (String uri : postLogoutRedirectUris) { + if (uri.equals(postLogoutRedirectionUri)) { + isValidPostLogoutRedirectUri = true; + break; + } + } + + if (!isValidPostLogoutRedirectUri) { + throw new OAuthAPIException("invalid_request", "The post_logout_redirect_uri is not valid for this client.", 400); + } + } + + // Validations are complete, time to respond + + if (postLogoutRedirectionUri == null && state == null && idTokenHint == null) { + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + response.addProperty("redirectTo", "{apiDomain}/fallbacks/logout/callback"); + super.sendJsonResponse(200, response, resp); + + return; } + String redirectTo = OAuth.createLogoutRequestAndReturnRedirectUri(main, appIdentifier, storage, clientId, postLogoutRedirectionUri, state, idTokenHint); + + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + response.addProperty("redirectTo", redirectTo); + super.sendJsonResponse(200, response, resp); + } catch (OAuthAPIException e) { OAuthProxyHelper.handleOAuthAPIException(resp, e); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | UnsupportedJWTSigningAlgorithmException | StorageTransactionLogicException 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 index 10252c230..42bf9dd84 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java @@ -8,7 +8,9 @@ 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; @@ -30,27 +32,16 @@ public String getPath() { @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String challenge = InputParser.parseStringOrThrowError(input, "challenge", false); 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) { + OAuth.deleteLogoutChallenge(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), challenge); + + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + super.sendJsonResponse(200, response, resp); + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException e) { throw new ServletException(e); } }