From 8438685187a7268bc09172319ba1d9b832916193 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 20 Aug 2024 17:57:40 +0530 Subject: [PATCH 01/40] fix: auth and token api --- devConfig.yaml | 8 +- .../supertokens/httpRequest/HttpRequest.java | 66 +++++++++++++- src/main/java/io/supertokens/oauth/OAuth.java | 31 ++++++- .../io/supertokens/webserver/Webserver.java | 2 + .../webserver/api/oauth/OAuthAuthAPI.java | 2 +- .../webserver/api/oauth/OAuthTokenAPI.java | 85 +++++++++++++++++++ 6 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java diff --git a/devConfig.yaml b/devConfig.yaml index 468752aa5..1b26e46c8 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -154,17 +154,17 @@ 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 # consent and login URLs to {apiDomain}. -# oauth_provider_consent_login_base_url: +oauth_provider_consent_login_base_url: http://localhost:3000 # (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: http://localhost:4001 \ No newline at end of file diff --git a/src/main/java/io/supertokens/httpRequest/HttpRequest.java b/src/main/java/io/supertokens/httpRequest/HttpRequest.java index 7edc8183f..73be6c072 100644 --- a/src/main/java/io/supertokens/httpRequest/HttpRequest.java +++ b/src/main/java/io/supertokens/httpRequest/HttpRequest.java @@ -125,6 +125,7 @@ public static T sendGETRequest(Main main, String requestID, String url, Map< @SuppressWarnings("unchecked") public static T sendGETRequestWithResponseHeaders(Main main, String requestID, String url, Map params, + Map headers, int connectionTimeoutMS, int readTimeoutMS, Integer version, Map> responseHeaders, boolean followRedirects) throws IOException, HttpResponseException { @@ -152,6 +153,11 @@ public static T sendGETRequestWithResponseHeaders(Main main, String requestI if (version != null) { con.setRequestProperty("api-version", version + ""); } + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + con.setRequestProperty(entry.getKey(), entry.getValue()); + } + } con.setInstanceFollowRedirects(followRedirects); int responseCode = con.getResponseCode(); @@ -293,4 +299,62 @@ public static T sendJsonDELETERequest(Main main, String requestID, String ur "DELETE"); } -} + public static T sendFormPOSTRequest(Main main, String requestID, String url, Map formData, + int connectionTimeoutMS, int readTimeoutMS, Integer version) + throws IOException, HttpResponseException { + StringBuilder formDataBuilder = new StringBuilder(); + for (Map.Entry entry : formData.entrySet()) { + formDataBuilder.append(entry.getKey()).append("=") + .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)).append("&"); + } + String formDataStr = formDataBuilder.toString(); + if (!formDataStr.equals("")) { + formDataStr = formDataStr.substring(0, formDataStr.length() - 1); + } + + URL obj = getURL(main, requestID, url); + HttpURLConnection con = null; + try { + con = (HttpURLConnection) obj.openConnection(); + con.setRequestMethod("POST"); + con.setConnectTimeout(connectionTimeoutMS); + con.setReadTimeout(readTimeoutMS); + con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + if (version != null) { + con.setRequestProperty("api-version", version + ""); + } + + con.setDoOutput(true); + try (OutputStream os = con.getOutputStream()) { + os.write(formDataStr.getBytes(StandardCharsets.UTF_8)); + } + + int responseCode = con.getResponseCode(); + InputStream inputStream = null; + if (responseCode < STATUS_CODE_ERROR_THRESHOLD) { + inputStream = con.getInputStream(); + } else { + inputStream = con.getErrorStream(); + } + + StringBuilder response = new StringBuilder(); + try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream))) { + String inputLine; + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + } + if (responseCode < STATUS_CODE_ERROR_THRESHOLD) { + if (!isJsonValid(response.toString())) { + return (T) response.toString(); + } + return (T) (new JsonParser().parse(response.toString())); + } + throw new HttpResponseException(responseCode, response.toString()); + } finally { + if (con != null) { + con.disconnect(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 6b7ef3488..bf0576023 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -51,6 +51,7 @@ public class OAuth { private static final String ERROR_DESCRIPTION_LITERAL = "error_description="; private static final String HYDRA_AUTH_ENDPOINT = "/oauth2/auth"; + private static final String HYDRA_TOKEN_ENDPOINT = "/oauth2/token"; private static final String HYDRA_CLIENTS_ENDPOINT = "/admin/clients"; public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject paramsFromSdk) @@ -66,16 +67,27 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app String hydraInternalAddress = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOauthProviderUrlConfiguredInHydra(); String hydraBaseUrlForConsentAndLogin = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOauthProviderConsentLoginBaseUrl(); - String clientId = paramsFromSdk.get("clientId").getAsString(); + String clientId = paramsFromSdk.get("client_id").getAsString(); + String cookie = null; + + if (paramsFromSdk.has("cookie")) { + cookie = paramsFromSdk.get("cookie").getAsString(); + paramsFromSdk.remove("cookie"); + } 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."); } else { // we query hydra Map queryParamsForHydra = constructHydraRequestParamsForAuthorizationGETAPICall(paramsFromSdk); + Map headers = new HashMap<>(); Map> responseHeaders = new HashMap<>(); - HttpRequest.sendGETRequestWithResponseHeaders(main, "", publicOAuthProviderServiceUrl + HYDRA_AUTH_ENDPOINT, queryParamsForHydra, 10000, 10000, null, responseHeaders, false); + if (cookie != null) { + headers.put("Cookie", cookie); + } + + HttpRequest.sendGETRequestWithResponseHeaders(main, "", publicOAuthProviderServiceUrl + HYDRA_AUTH_ENDPOINT, queryParamsForHydra, headers, 10000, 10000, null, responseHeaders, false); if(!responseHeaders.isEmpty() && responseHeaders.containsKey(LOCATION_HEADER_NAME)) { String locationHeaderValue = responseHeaders.get(LOCATION_HEADER_NAME).get(0); @@ -101,6 +113,19 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app return new OAuthAuthResponse(redirectTo, cookies); } + public static JsonObject getToken(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject bodyFromSdk) throws InvalidConfigException, TenantOrAppNotFoundException { + String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); + try { + Map bodyParams = constructHydraRequestParamsForAuthorizationGETAPICall(bodyFromSdk); + JsonObject response = HttpRequest.sendFormPOSTRequest(main, "", publicOAuthProviderServiceUrl + HYDRA_TOKEN_ENDPOINT, bodyParams, 10000, 10000, null); + // TODO: token transformations + // TODO: error handling + return response; + } catch (HttpResponseException | IOException e) { + throw new RuntimeException(e); + } + } + //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, @@ -289,7 +314,7 @@ private static JsonObject formatResponseForSDK(JsonObject response) { 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()); + queryParamsForHydra.put(jsonElement.getKey(), jsonElement.getValue().getAsString()); } return queryParamsForHydra; } diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index 45c262a64..fe3f9c3ec 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -41,6 +41,7 @@ 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.OAuthTokenAPI; import io.supertokens.webserver.api.passwordless.*; import io.supertokens.webserver.api.session.*; import io.supertokens.webserver.api.thirdparty.GetUsersByEmailAPI; @@ -270,6 +271,7 @@ private void setupRoutes() { addAPI(new GetTenantCoreConfigForDashboardAPI(main)); addAPI(new OAuthAuthAPI(main)); + addAPI(new OAuthTokenAPI(main)); addAPI(new OAuthClientsAPI(main)); StandardContext context = tomcatReference.getContext(); 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..9c897b89f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -48,7 +48,7 @@ public OAuthAuthAPI(Main main) { super(main, RECIPE_ID.OAUTH.toString()); } - private static final List REQUIRED_FIELDS_FOR_POST = Arrays.asList(new String[]{"clientId", "responseType"}); + private static final List REQUIRED_FIELDS_FOR_POST = Arrays.asList(new String[]{"client_id", "response_type"}); @Override public String getPath() { 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..0d70c5474 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.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 com.google.gson.*; +import io.supertokens.Main; +import io.supertokens.httpRequest.HttpResponseException; +import io.supertokens.multitenancy.exception.BadPermissionException; +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; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.Serial; +import java.util.Arrays; +import java.util.List; + +public class OAuthTokenAPI extends WebserverAPI { + @Serial + private static final long serialVersionUID = -8734479943734920904L; + + 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); + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + JsonObject response = OAuth.getToken(super.main, appIdentifier, storage, + input); + + response.addProperty("status", "OK"); + super.sendJsonResponse(200, response, resp); + + // } catch (OAuthAuthException authException) { + + // 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); + + } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException e) { + throw new ServletException(e); + } catch (Exception e) { + throw new ServletException(e); + } + } +} From 48cfb7a9715e59cdaea0fab19423f765417c188f Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 21 Aug 2024 20:30:25 +0530 Subject: [PATCH 02/40] fix: cookie and code transformations --- src/main/java/io/supertokens/oauth/OAuth.java | 16 ++++++++++++++-- .../io/supertokens/test/HttpRequestTest.java | 2 +- .../io/supertokens/test/JWKSPublicAPITest.java | 4 ++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index bf0576023..13685fe6c 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -72,6 +72,7 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app if (paramsFromSdk.has("cookie")) { cookie = paramsFromSdk.get("cookie").getAsString(); + cookie = cookie.replaceAll("st_oauth_", "ory_hydra_"); paramsFromSdk.remove("cookie"); } @@ -89,7 +90,7 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app HttpRequest.sendGETRequestWithResponseHeaders(main, "", publicOAuthProviderServiceUrl + HYDRA_AUTH_ENDPOINT, queryParamsForHydra, headers, 10000, 10000, null, responseHeaders, false); - if(!responseHeaders.isEmpty() && responseHeaders.containsKey(LOCATION_HEADER_NAME)) { + 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); @@ -102,11 +103,21 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app } else { redirectTo = locationHeaderValue; } + if (redirectTo.contains("code=ory_ac_")) { + redirectTo = redirectTo.replace("code=ory_ac_", "code=st_ac_"); + } } else { throw new RuntimeException("Unexpected answer from Oauth Provider"); } if(responseHeaders.containsKey(COOKIES_HEADER_NAME)){ - cookies = responseHeaders.get(COOKIES_HEADER_NAME); + cookies = new ArrayList<>(responseHeaders.get(COOKIES_HEADER_NAME)); + + for (int i = 0; i < cookies.size(); i++) { + String cookieStr = cookies.get(i); + if (cookieStr.startsWith("ory_hydra_")) { + cookies.set(i, "st_oauth_" + cookieStr.substring(10)); + } + } } } @@ -117,6 +128,7 @@ public static JsonObject getToken(Main main, AppIdentifier appIdentifier, Storag String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); try { Map bodyParams = constructHydraRequestParamsForAuthorizationGETAPICall(bodyFromSdk); + bodyParams.put("code", bodyParams.get("code").replace("st_ac_", "ory_ac_")); JsonObject response = HttpRequest.sendFormPOSTRequest(main, "", publicOAuthProviderServiceUrl + HYDRA_TOKEN_ENDPOINT, bodyParams, 10000, 10000, null); // TODO: token transformations // TODO: error handling diff --git a/src/test/java/io/supertokens/test/HttpRequestTest.java b/src/test/java/io/supertokens/test/HttpRequestTest.java index 98213b3ab..eb0dac22b 100644 --- a/src/test/java/io/supertokens/test/HttpRequestTest.java +++ b/src/test/java/io/supertokens/test/HttpRequestTest.java @@ -778,7 +778,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO { String response = HttpRequest.sendGETRequestWithResponseHeaders(process.getProcess(), "", - "http://localhost:3567/getTestWithHeaders", null, 1000, 1000, null, responseHeaders, true); + "http://localhost:3567/getTestWithHeaders", new HashMap<>(), null, 1000, 1000, null, responseHeaders, true); assertEquals(response, "200"); assertTrue(responseHeaders.containsKey("SomeNameForHeader")); assertEquals(responseHeaders.get("SomeNameForHeader"), Collections.singletonList("someValueForHeader")); diff --git a/src/test/java/io/supertokens/test/JWKSPublicAPITest.java b/src/test/java/io/supertokens/test/JWKSPublicAPITest.java index a62c7c4f7..9b13d6d2c 100644 --- a/src/test/java/io/supertokens/test/JWKSPublicAPITest.java +++ b/src/test/java/io/supertokens/test/JWKSPublicAPITest.java @@ -82,7 +82,7 @@ public void testCacheControlValue() throws Exception { // check regular output Map> responseHeaders = new HashMap<>(); JsonObject response = HttpRequest.sendGETRequestWithResponseHeaders(process.getProcess(), "", - "http://localhost:3567/.well-known/jwks.json", null, + "http://localhost:3567/.well-known/jwks.json", null, new HashMap<>(), 1000, 1000, null, responseHeaders, true); assertEquals(response.entrySet().size(), 1); @@ -97,7 +97,7 @@ public void testCacheControlValue() throws Exception { Thread.sleep(2000); response = HttpRequest.sendGETRequestWithResponseHeaders(process.getProcess(), "", - "http://localhost:3567/.well-known/jwks.json", null, + "http://localhost:3567/.well-known/jwks.json", null, new HashMap<>(), 1000, 1000, null, responseHeaders, true); assertEquals(response.entrySet().size(), 1); From fa44c0cc4795786d149a3df14b67236151bf5b76 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 23 Aug 2024 16:49:14 +0530 Subject: [PATCH 03/40] fix: token re-signing --- src/main/java/io/supertokens/oauth/OAuth.java | 100 +++++++++++++++++- .../java/io/supertokens/session/jwt/JWT.java | 14 +++ src/main/java/io/supertokens/utils/Utils.java | 26 +++++ .../webserver/api/oauth/OAuthTokenAPI.java | 28 ++--- 4 files changed, 155 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 13685fe6c..012c7b744 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -16,6 +16,7 @@ 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; @@ -24,23 +25,33 @@ import io.supertokens.config.Config; import io.supertokens.httpRequest.HttpRequest; import io.supertokens.httpRequest.HttpResponseException; +import io.supertokens.jwt.JWTSigningFunctions; +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.jwt.JWTSigningKeyInfo; 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; +import io.supertokens.session.jwt.JWT.JWTException; +import io.supertokens.signingkeys.JWTSigningKey; +import io.supertokens.signingkeys.SigningKeys; 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.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; import java.util.*; public class OAuth { @@ -124,18 +135,105 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app return new OAuthAuthResponse(redirectTo, cookies); } - public static JsonObject getToken(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject bodyFromSdk) throws InvalidConfigException, TenantOrAppNotFoundException { + public static JsonObject getToken(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject bodyFromSdk, boolean useDynamicKey) throws InvalidConfigException, TenantOrAppNotFoundException, OAuthAuthException, StorageQueryException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + String clientId = bodyFromSdk.get("client_id").getAsString(); + + 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 publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); try { Map bodyParams = constructHydraRequestParamsForAuthorizationGETAPICall(bodyFromSdk); bodyParams.put("code", bodyParams.get("code").replace("st_ac_", "ory_ac_")); JsonObject response = HttpRequest.sendFormPOSTRequest(main, "", publicOAuthProviderServiceUrl + HYDRA_TOKEN_ENDPOINT, bodyParams, 10000, 10000, null); + + // token transformations + if (response.has("access_token")) { + String accessToken = response.get("access_token").getAsString(); + accessToken = resignToken(appIdentifier, main, accessToken, 1, useDynamicKey); + response.addProperty("access_token", accessToken); + } + + if (response.has("id_token")) { + String idToken = response.get("id_token").getAsString(); + idToken = resignToken(appIdentifier, main, idToken, 2, useDynamicKey); + response.addProperty("id_token", idToken); + } // TODO: token transformations // TODO: error handling return response; } catch (HttpResponseException | IOException e) { + // TODO Auto-generated catch block + throw new RuntimeException(e); + } catch (InvalidKeyException e) { + // TODO Auto-generated catch block + throw new RuntimeException(e); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + throw new RuntimeException(e); + } catch (JWTException e) { + // TODO Auto-generated catch block + throw new RuntimeException(e); + } catch (InvalidKeySpecException e) { + // TODO Auto-generated catch block throw new RuntimeException(e); + } catch (JWTCreationException e) { + // TODO Auto-generated catch block + throw new RuntimeException(e); + } catch (StorageTransactionLogicException e) { + // TODO Auto-generated catch block + throw new RuntimeException(e); + } catch (UnsupportedJWTSigningAlgorithmException e) { + // TODO Auto-generated catch block + throw new RuntimeException(e); + } + } + + private static String resignToken(AppIdentifier appIdentifier, Main main, String token, int stt, boolean useDynamicSigningKey) throws IOException, HttpResponseException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException { + // Load the JWKS from the specified URL + String jwksUrl = "http://localhost:4444/.well-known/jwks.json"; + JsonObject jwksResponse = HttpRequest.sendGETRequest(main, "", jwksUrl, null, 10000, 10000, null); + JsonArray keys = jwksResponse.get("keys").getAsJsonArray(); + + // Validate the JWT and extract claims using the fetched public signing keys + JWT.JWTPreParseInfo jwtInfo = JWT.preParseJWTInfo(token); + JWT.JWTInfo jwtResult = null; + for (JsonElement key : keys) { + JsonObject keyObject = key.getAsJsonObject(); + String kid = keyObject.get("kid").getAsString(); + if (jwtInfo.kid.equals(kid)) { + jwtResult = JWT.verifyJWTAndGetPayload(jwtInfo, keyObject.get("n").getAsString(), keyObject.get("e").getAsString()); + break; + } } + if (jwtResult == null) { + throw new RuntimeException("No matching key found for JWT verification"); + } + JsonObject payload = jwtResult.payload; + // move keys in ext to root + if (payload.has("ext")) { + JsonObject ext = payload.getAsJsonObject("ext"); + for (Map.Entry entry : ext.entrySet()) { + payload.add(entry.getKey(), entry.getValue()); + } + payload.remove("ext"); + } + payload.addProperty("stt", stt); + + 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; } //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 diff --git a/src/main/java/io/supertokens/session/jwt/JWT.java b/src/main/java/io/supertokens/session/jwt/JWT.java index 05b8288c0..aa75e7a92 100644 --- a/src/main/java/io/supertokens/session/jwt/JWT.java +++ b/src/main/java/io/supertokens/session/jwt/JWT.java @@ -134,6 +134,20 @@ public static JWTInfo verifyJWTAndGetPayload(JWTPreParseInfo jwt, String publicS return new JWTInfo(new JsonParser().parse(Utils.convertFromBase64(jwt.payload)).getAsJsonObject(), jwt.version); } + public static JWTInfo verifyJWTAndGetPayload(JWTPreParseInfo jwt, String n, String e) + throws InvalidKeyException, NoSuchAlgorithmException, JWTException { + + try { + if (!Utils.verifyWithPublicKey(jwt.header + "." + jwt.payload, jwt.signature, n, e, + jwt.version != AccessToken.VERSION.V1 && jwt.version != AccessToken.VERSION.V2)) { + throw new JWTException("JWT verification failed"); + } + } catch (InvalidKeySpecException | SignatureException e2) { + throw new JWTException("JWT verification failed"); + } + return new JWTInfo(new JsonParser().parse(Utils.convertFromBase64(jwt.payload)).getAsJsonObject(), jwt.version); + } + public static JWTInfo getPayloadWithoutVerifying(String jwt) throws JWTException { JWTPreParseInfo jwtInfo = preParseJWTInfo(jwt); return new JWTInfo(new JsonParser().parse(Utils.convertFromBase64(jwtInfo.payload)).getAsJsonObject(), diff --git a/src/main/java/io/supertokens/utils/Utils.java b/src/main/java/io/supertokens/utils/Utils.java index 07ea2e5ff..5a4936710 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; @@ -315,6 +316,31 @@ public static boolean verifyWithPublicKey(String content, String signature, Stri return sign.verify(decoder.decode(signature)); } + public static boolean verifyWithPublicKey(String content, String signature, String n, String e, boolean urlEncoded) + throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException { + Signature sign = Signature.getInstance("SHA256withRSA"); + byte[] modulusBytes = Base64.getUrlDecoder().decode(n); + byte[] exponentBytes = Base64.getUrlDecoder().decode(e); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + + if ((modulusBytes[0] & 0x80) != 0) { + byte[] newModulusBytes = new byte[modulusBytes.length + 1]; + System.arraycopy(modulusBytes, 0, newModulusBytes, 1, modulusBytes.length); + modulusBytes = newModulusBytes; + } + + BigInteger modulus = new BigInteger(modulusBytes); + BigInteger exponent = new BigInteger(1, exponentBytes); + + RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); + PublicKey pub = keyFactory.generatePublic(spec); + + Base64.Decoder signatureDecoder = urlEncoded ? Base64.getUrlDecoder() : Base64.getDecoder(); + sign.initVerify(pub); + sign.update(content.getBytes()); + return sign.verify(signatureDecoder.decode(signature)); + } + public static boolean isFakeEmail(String email) { return email.endsWith("@stfakeemail.supertokens.com") || email.endsWith(".fakeemail.com"); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 0d70c5474..afa89cdb1 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -18,7 +18,6 @@ import com.google.gson.*; import io.supertokens.Main; -import io.supertokens.httpRequest.HttpResponseException; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.OAuth; import io.supertokens.oauth.exceptions.OAuthAuthException; @@ -28,7 +27,6 @@ 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; @@ -37,8 +35,6 @@ import java.io.IOException; import java.io.Serial; -import java.util.Arrays; -import java.util.List; public class OAuthTokenAPI extends WebserverAPI { @Serial @@ -58,27 +54,35 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + boolean useDynamicKey = false; + Boolean useStaticKeyInput = InputParser.parseBooleanOrThrowError(input, "useStaticSigningKey", true); + // useStaticKeyInput defaults to true, so we check if it has been explicitly set to false + useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); + + JsonObject bodyFromSDK = InputParser.parseJsonObjectOrThrowError(input, "body", false); + + try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); JsonObject response = OAuth.getToken(super.main, appIdentifier, storage, - input); + bodyFromSDK, useDynamicKey); response.addProperty("status", "OK"); super.sendJsonResponse(200, response, resp); - // } catch (OAuthAuthException authException) { + } catch (OAuthAuthException authException) { - // 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 errorResponse = new JsonObject(); + errorResponse.addProperty("error", authException.error); + errorResponse.addProperty("errorDescription", authException.errorDescription); + errorResponse.addProperty("status", "OAUTH2_AUTH_ERROR"); + super.sendJsonResponse(200, errorResponse, resp); } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException e) { throw new ServletException(e); - } catch (Exception e) { + } catch (StorageQueryException e) { throw new ServletException(e); } } From cb790642572b9dbbd6097c17effcf552bee284fd Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 26 Aug 2024 17:46:41 +0530 Subject: [PATCH 04/40] fix: token endpoint --- src/main/java/io/supertokens/oauth/OAuth.java | 88 +++++++++---------- ...hException.java => OAuthAPIException.java} | 6 +- .../OAuthAPIInvalidInputException.java | 4 +- .../OAuthClientNotFoundException.java | 4 +- .../OAuthClientUpdateException.java | 4 +- .../oauth/exceptions/OAuthException.java | 4 +- .../webserver/api/oauth/OAuthAuthAPI.java | 12 +-- .../webserver/api/oauth/OAuthTokenAPI.java | 24 +++-- .../test/oauth/api/OAuthAuthAPITest.java | 10 +-- 9 files changed, 80 insertions(+), 76 deletions(-) rename src/main/java/io/supertokens/oauth/exceptions/{OAuthAuthException.java => OAuthAPIException.java} (81%) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 012c7b744..a809ea2e7 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -17,6 +17,7 @@ package io.supertokens.oauth; import com.auth0.jwt.exceptions.JWTCreationException; +import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -64,9 +65,10 @@ public class OAuth { private static final String HYDRA_AUTH_ENDPOINT = "/oauth2/auth"; private static final String HYDRA_TOKEN_ENDPOINT = "/oauth2/token"; private static final String HYDRA_CLIENTS_ENDPOINT = "/admin/clients"; + private static final String HYDRA_JWKS_PATH = "/.well-known/jwks.json"; // New constant for JWKS path - public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject paramsFromSdk) - throws InvalidConfigException, HttpResponseException, IOException, OAuthAuthException, StorageQueryException, + public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject paramsFromSdk, String inputCookies) + throws InvalidConfigException, HttpResponseException, IOException, OAuthAPIException, StorageQueryException, TenantOrAppNotFoundException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -79,24 +81,21 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app String hydraBaseUrlForConsentAndLogin = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOauthProviderConsentLoginBaseUrl(); String clientId = paramsFromSdk.get("client_id").getAsString(); - String cookie = null; - if (paramsFromSdk.has("cookie")) { - cookie = paramsFromSdk.get("cookie").getAsString(); - cookie = cookie.replaceAll("st_oauth_", "ory_hydra_"); - paramsFromSdk.remove("cookie"); + if (inputCookies != null) { + inputCookies = inputCookies.replaceAll("st_oauth_", "ory_hydra_"); } 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."); + throw new OAuthAPIException("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.", 400); } else { // we query hydra Map queryParamsForHydra = constructHydraRequestParamsForAuthorizationGETAPICall(paramsFromSdk); Map headers = new HashMap<>(); Map> responseHeaders = new HashMap<>(); - if (cookie != null) { - headers.put("Cookie", cookie); + if (inputCookies != null) { + headers.put("Cookie", inputCookies); } HttpRequest.sendGETRequestWithResponseHeaders(main, "", publicOAuthProviderServiceUrl + HYDRA_AUTH_ENDPOINT, queryParamsForHydra, headers, 10000, 10000, null, responseHeaders, false); @@ -106,7 +105,7 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app if(Utils.containsUrl(locationHeaderValue, hydraInternalAddress, true)){ String error = getValueOfQueryParam(locationHeaderValue, ERROR_LITERAL); String errorDescription = getValueOfQueryParam(locationHeaderValue, ERROR_DESCRIPTION_LITERAL); - throw new OAuthAuthException(error, errorDescription); + throw new OAuthAPIException(error, errorDescription, 400); } if(Utils.containsUrl(locationHeaderValue, hydraBaseUrlForConsentAndLogin, true)){ @@ -135,65 +134,59 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app return new OAuthAuthResponse(redirectTo, cookies); } - public static JsonObject getToken(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject bodyFromSdk, boolean useDynamicKey) throws InvalidConfigException, TenantOrAppNotFoundException, OAuthAuthException, StorageQueryException { + public static JsonObject getToken(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject bodyFromSdk, String iss, boolean useDynamicKey) throws InvalidConfigException, TenantOrAppNotFoundException, OAuthAPIException, StorageQueryException, IOException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, JWTCreationException, JWTException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); String clientId = bodyFromSdk.get("client_id").getAsString(); 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."); + throw new OAuthAPIException("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.", 400); } String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); try { Map bodyParams = constructHydraRequestParamsForAuthorizationGETAPICall(bodyFromSdk); - bodyParams.put("code", bodyParams.get("code").replace("st_ac_", "ory_ac_")); + if (bodyParams.containsKey("code")) { + bodyParams.put("code", bodyParams.get("code").replace("st_ac_", "ory_ac_")); + } + if (bodyParams.containsKey("refresh_token")) { + bodyParams.put("refresh_token", bodyParams.get("refresh_token").replace("st_rt_", "ory_rt_")); + } JsonObject response = HttpRequest.sendFormPOSTRequest(main, "", publicOAuthProviderServiceUrl + HYDRA_TOKEN_ENDPOINT, bodyParams, 10000, 10000, null); // token transformations if (response.has("access_token")) { String accessToken = response.get("access_token").getAsString(); - accessToken = resignToken(appIdentifier, main, accessToken, 1, useDynamicKey); + accessToken = resignToken(appIdentifier, main, accessToken, iss, 1, useDynamicKey); response.addProperty("access_token", accessToken); } if (response.has("id_token")) { String idToken = response.get("id_token").getAsString(); - idToken = resignToken(appIdentifier, main, idToken, 2, useDynamicKey); + idToken = resignToken(appIdentifier, main, idToken, iss, 2, useDynamicKey); response.addProperty("id_token", idToken); } - // TODO: token transformations - // TODO: error handling + + if (response.has("refresh_token")) { + String refreshToken = response.get("refresh_token").getAsString(); + refreshToken = refreshToken.replace("ory_rt_", "st_rt_"); + response.addProperty("refresh_token", refreshToken); + } return response; - } catch (HttpResponseException | IOException e) { - // TODO Auto-generated catch block - throw new RuntimeException(e); - } catch (InvalidKeyException e) { - // TODO Auto-generated catch block - throw new RuntimeException(e); - } catch (NoSuchAlgorithmException e) { - // TODO Auto-generated catch block - throw new RuntimeException(e); - } catch (JWTException e) { - // TODO Auto-generated catch block - throw new RuntimeException(e); - } catch (InvalidKeySpecException e) { - // TODO Auto-generated catch block - throw new RuntimeException(e); - } catch (JWTCreationException e) { - // TODO Auto-generated catch block - throw new RuntimeException(e); - } catch (StorageTransactionLogicException e) { - // TODO Auto-generated catch block - throw new RuntimeException(e); - } catch (UnsupportedJWTSigningAlgorithmException e) { - // TODO Auto-generated catch block - throw new RuntimeException(e); + + } catch (HttpResponseException e) { + JsonObject errorResponse = new Gson().fromJson(e.rawMessage, JsonObject.class); + throw new OAuthAPIException( + errorResponse.get("error").getAsString(), + errorResponse.get("error_description").getAsString(), + e.statusCode + ); } } - private static String resignToken(AppIdentifier appIdentifier, Main main, String token, int stt, boolean useDynamicSigningKey) throws IOException, HttpResponseException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException { + private static String resignToken(AppIdentifier appIdentifier, Main main, String token, String iss, int stt, boolean useDynamicSigningKey) throws IOException, HttpResponseException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { // Load the JWKS from the specified URL - String jwksUrl = "http://localhost:4444/.well-known/jwks.json"; + String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); + String jwksUrl = publicOAuthProviderServiceUrl + HYDRA_JWKS_PATH; // Use the new constant JsonObject jwksResponse = HttpRequest.sendGETRequest(main, "", jwksUrl, null, 10000, 10000, null); JsonArray keys = jwksResponse.get("keys").getAsJsonArray(); @@ -220,6 +213,7 @@ private static String resignToken(AppIdentifier appIdentifier, Main main, String } payload.remove("ext"); } + payload.addProperty("iss", iss); payload.addProperty("stt", stt); JWTSigningKeyInfo keyToUse; @@ -287,7 +281,7 @@ public static JsonObject loadOAuthClient(Main main, AppIdentifier appIdentifier, String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException("Unable to locate the resource", ""); + throw new OAuthClientNotFoundException("Unable to locate the resource", "", 400); } else { try { JsonObject hydraResponse = HttpRequest.sendGETRequest(main, "", adminOAuthProviderServiceUrl + HYDRA_CLIENTS_ENDPOINT + "/" + clientId, null, 10000, 10000, null); @@ -311,7 +305,7 @@ public static void deleteOAuthClient(Main main, AppIdentifier appIdentifier, Sto String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException("Unable to locate the resource", ""); + throw new OAuthClientNotFoundException("Unable to locate the resource", "", 400); } else { try { oauthStorage.removeAppClientAssociation(appIdentifier, clientId); @@ -339,7 +333,7 @@ public static JsonObject updateOauthClient(Main main, AppIdentifier appIdentifie String clientId = paramsFromSdk.get("clientId").getAsString(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException("Unable to locate the resource", ""); + throw new OAuthClientNotFoundException("Unable to locate the resource", "", 400); } else { JsonArray hydraInput = translateIncomingDataToHydraUpdateFormat(paramsFromSdk); try { diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthAuthException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java similarity index 81% rename from src/main/java/io/supertokens/oauth/exceptions/OAuthAuthException.java rename to src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java index ba850d287..6cfb5a523 100644 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthAuthException.java +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java @@ -16,10 +16,10 @@ package io.supertokens.oauth.exceptions; -public class OAuthAuthException extends OAuthException{ +public class OAuthAPIException extends OAuthException{ private static final long serialVersionUID = 1836718299845759897L; - public OAuthAuthException(String error, String errorDescription) { - super(error, errorDescription); + public OAuthAPIException(String error, String errorDescription, int statusCode) { + super(error, errorDescription, statusCode); } } diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java index 91634a198..3f66d9f27 100644 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java @@ -23,7 +23,7 @@ public class OAuthAPIInvalidInputException extends OAuthException{ @Serial private static final long serialVersionUID = 665027786586190611L; - public OAuthAPIInvalidInputException(String error, String errorDescription) { - super(error, errorDescription); + public OAuthAPIInvalidInputException(String error, String errorDescription, int statusCode) { + super(error, errorDescription, statusCode); } } diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java index 77b3df4f7..c8697c9da 100644 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java @@ -22,7 +22,7 @@ public class OAuthClientNotFoundException extends OAuthException{ @Serial private static final long serialVersionUID = 1412853176388698991L; - public OAuthClientNotFoundException(String error, String errorDescription) { - super(error, errorDescription); + public OAuthClientNotFoundException(String error, String errorDescription, int statusCode) { + super(error, errorDescription, statusCode); } } diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientUpdateException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthClientUpdateException.java index bc03389f2..a86aa3a27 100644 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientUpdateException.java +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthClientUpdateException.java @@ -22,7 +22,7 @@ public class OAuthClientUpdateException extends OAuthException{ @Serial private static final long serialVersionUID = -5191044905397936167L; - public OAuthClientUpdateException(String error, String errorDescription) { - super(error, errorDescription); + public OAuthClientUpdateException(String error, String errorDescription, int statusCode) { + super(error, errorDescription, statusCode); } } diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthException.java index 890cb02cb..fe8252391 100644 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthException.java +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthException.java @@ -24,10 +24,12 @@ public class OAuthException extends Exception{ public final String error; public final String errorDescription; + public final int statusCode; - public OAuthException(String error, String errorDescription){ + public OAuthException(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/webserver/api/oauth/OAuthAuthAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java index 9c897b89f..eaef1b8b7 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -21,7 +21,7 @@ import io.supertokens.httpRequest.HttpResponseException; import io.supertokens.multitenancy.exception.BadPermissionException; 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.Storage; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; @@ -59,14 +59,16 @@ public String getPath() { 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); + + InputParser.throwErrorOnMissingRequiredField(params, REQUIRED_FIELDS_FOR_POST); try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - OAuthAuthResponse authResponse = OAuth.getAuthorizationUrl(super.main, appIdentifier, storage, - input); + OAuthAuthResponse authResponse = OAuth.getAuthorizationUrl(super.main, appIdentifier, storage, params, cookies); JsonObject response = new JsonObject(); response.addProperty("redirectTo", authResponse.redirectTo); @@ -80,7 +82,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I response.addProperty("status", "OK"); super.sendJsonResponse(200, response, resp); - } catch (OAuthAuthException authException) { + } catch (OAuthAPIException authException) { JsonObject errorResponse = new JsonObject(); errorResponse.addProperty("error", authException.error); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index afa89cdb1..2d724abef 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -16,17 +16,21 @@ package io.supertokens.webserver.api.oauth; +import com.auth0.jwt.exceptions.JWTCreationException; import com.google.gson.*; import io.supertokens.Main; +import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; import io.supertokens.multitenancy.exception.BadPermissionException; 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.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; @@ -35,6 +39,9 @@ import java.io.IOException; import java.io.Serial; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; public class OAuthTokenAPI extends WebserverAPI { @Serial @@ -53,6 +60,7 @@ public String getPath() { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String iss = InputParser.parseStringOrThrowError(input, "iss", false); boolean useDynamicKey = false; Boolean useStaticKeyInput = InputParser.parseBooleanOrThrowError(input, "useStaticSigningKey", true); @@ -61,28 +69,26 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject bodyFromSDK = InputParser.parseJsonObjectOrThrowError(input, "body", false); - try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); JsonObject response = OAuth.getToken(super.main, appIdentifier, storage, - bodyFromSDK, useDynamicKey); + bodyFromSDK, iss, useDynamicKey); response.addProperty("status", "OK"); super.sendJsonResponse(200, response, resp); - } catch (OAuthAuthException authException) { + } catch (OAuthAPIException authException) { JsonObject errorResponse = new JsonObject(); errorResponse.addProperty("error", authException.error); - errorResponse.addProperty("errorDescription", authException.errorDescription); - errorResponse.addProperty("status", "OAUTH2_AUTH_ERROR"); + errorResponse.addProperty("error_description", authException.errorDescription); + errorResponse.addProperty("status_code", authException.statusCode); + errorResponse.addProperty("status", "OAUTH2_TOKEN_ERROR"); super.sendJsonResponse(200, errorResponse, resp); - } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException e) { - throw new ServletException(e); - } catch (StorageQueryException e) { + } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { throw new ServletException(e); } } diff --git a/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java b/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java index 424574468..e0ccb9d6b 100644 --- a/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java +++ b/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java @@ -20,7 +20,7 @@ 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; @@ -59,7 +59,7 @@ public void beforeEach() throws InterruptedException { @Test public void testLocalhostChangedToApiDomain() - throws StorageQueryException, OAuthAuthException, HttpResponseException, TenantOrAppNotFoundException, + throws StorageQueryException, OAuthAPIException, HttpResponseException, TenantOrAppNotFoundException, InvalidConfigException, IOException, OAuth2ClientAlreadyExistsForAppException, io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { @@ -144,7 +144,7 @@ public void testCalledWithWrongClientIdNotInST_exceptionThrown() AppIdentifier testApp = new AppIdentifier("", ""); oAuthStorage.addClientForApp(testApp, clientId); - OAuthAuthException thrown = assertThrows(OAuthAuthException.class, () -> { + OAuthAPIException thrown = assertThrows(OAuthAPIException.class, () -> { OAuthAuthResponse response = OAuth.getAuthorizationUrl(process.getProcess(), new AppIdentifier("", ""), oAuthStorage, requestBody); @@ -201,7 +201,7 @@ public void testCalledWithWrongClientIdNotInHydraButInST_exceptionThrown() AppIdentifier testApp = new AppIdentifier("", ""); oAuthStorage.addClientForApp(testApp, clientId); - OAuthAuthException thrown = assertThrows(OAuthAuthException.class, () -> { + OAuthAPIException thrown = assertThrows(OAuthAPIException.class, () -> { OAuthAuthResponse response = OAuth.getAuthorizationUrl(process.getProcess(), new AppIdentifier("", ""), oAuthStorage, requestBody); @@ -259,7 +259,7 @@ public void testCalledWithWrongRedirectUrl_exceptionThrown() AppIdentifier testApp = new AppIdentifier("", ""); oAuthStorage.addClientForApp(testApp, clientId); - OAuthAuthException thrown = assertThrows(OAuthAuthException.class, () -> { + OAuthAPIException thrown = assertThrows(OAuthAPIException.class, () -> { OAuthAuthResponse response = OAuth.getAuthorizationUrl(process.getProcess(), new AppIdentifier("", ""), oAuthStorage, requestBody); From 393146bab942c50916811202c9e8176eaff6bdfa Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 26 Aug 2024 18:55:00 +0530 Subject: [PATCH 05/40] fix: license check and jwks caching --- src/main/java/io/supertokens/oauth/OAuth.java | 73 +++++++++++++++---- .../webserver/api/oauth/OAuthAuthAPI.java | 3 +- .../webserver/api/oauth/OAuthTokenAPI.java | 6 +- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index a809ea2e7..7945fba5e 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -24,6 +24,9 @@ import com.google.gson.JsonParser; import io.supertokens.Main; import io.supertokens.config.Config; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.httpRequest.HttpRequest; import io.supertokens.httpRequest.HttpResponseException; import io.supertokens.jwt.JWTSigningFunctions; @@ -67,9 +70,27 @@ public class OAuth { private static final String HYDRA_CLIENTS_ENDPOINT = "/admin/clients"; private static final String HYDRA_JWKS_PATH = "/.well-known/jwks.json"; // New constant for JWKS path + private static Map> jwksCache = new HashMap<>(); // Cache for JWKS keys + private static final int MAX_RETRIES = 3; // Maximum number of retries for fetching JWKS + + 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 OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject paramsFromSdk, String inputCookies) throws InvalidConfigException, HttpResponseException, IOException, OAuthAPIException, StorageQueryException, - TenantOrAppNotFoundException { + TenantOrAppNotFoundException, FeatureNotEnabledException { + + checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -134,7 +155,9 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app return new OAuthAuthResponse(redirectTo, cookies); } - public static JsonObject getToken(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject bodyFromSdk, String iss, boolean useDynamicKey) throws InvalidConfigException, TenantOrAppNotFoundException, OAuthAPIException, StorageQueryException, IOException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, JWTCreationException, JWTException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException { + public static JsonObject getToken(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject bodyFromSdk, String iss, boolean useDynamicKey) throws InvalidConfigException, TenantOrAppNotFoundException, OAuthAPIException, StorageQueryException, IOException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, JWTCreationException, JWTException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, FeatureNotEnabledException { + checkForOauthFeature(appIdentifier, main); + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); String clientId = bodyFromSdk.get("client_id").getAsString(); @@ -156,13 +179,13 @@ public static JsonObject getToken(Main main, AppIdentifier appIdentifier, Storag // token transformations if (response.has("access_token")) { String accessToken = response.get("access_token").getAsString(); - accessToken = resignToken(appIdentifier, main, accessToken, iss, 1, useDynamicKey); + accessToken = reSignToken(appIdentifier, main, accessToken, iss, 1, useDynamicKey, 0); response.addProperty("access_token", accessToken); } if (response.has("id_token")) { String idToken = response.get("id_token").getAsString(); - idToken = resignToken(appIdentifier, main, idToken, iss, 2, useDynamicKey); + idToken = reSignToken(appIdentifier, main, idToken, iss, 2, useDynamicKey, 0); response.addProperty("id_token", idToken); } @@ -183,27 +206,47 @@ public static JsonObject getToken(Main main, AppIdentifier appIdentifier, Storag } } - private static String resignToken(AppIdentifier appIdentifier, Main main, String token, String iss, int stt, boolean useDynamicSigningKey) throws IOException, HttpResponseException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { + private static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, int stt, boolean useDynamicSigningKey, int retryCount) throws IOException, HttpResponseException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException, OAuthAPIException { // Load the JWKS from the specified URL String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); String jwksUrl = publicOAuthProviderServiceUrl + HYDRA_JWKS_PATH; // Use the new constant - JsonObject jwksResponse = HttpRequest.sendGETRequest(main, "", jwksUrl, null, 10000, 10000, null); - JsonArray keys = jwksResponse.get("keys").getAsJsonArray(); + + // Check if cached keys are available for the jwksUrl + Map cachedKeys = jwksCache.get(jwksUrl); + if (cachedKeys == null) { + JsonObject jwksResponse = HttpRequest.sendGETRequest(main, "", jwksUrl, null, 10000, 10000, null); + cachedKeys = new HashMap<>(); + JsonArray keysArray = jwksResponse.get("keys").getAsJsonArray(); + + // Populate the cache with keys indexed by kid + for (JsonElement key : keysArray) { + JsonObject keyObject = key.getAsJsonObject(); + String kid = keyObject.get("kid").getAsString(); + cachedKeys.put(kid, keyObject); + } + jwksCache.put(jwksUrl, cachedKeys); // Cache the keys with jwksUrl as the key + } // Validate the JWT and extract claims using the fetched public signing keys JWT.JWTPreParseInfo jwtInfo = JWT.preParseJWTInfo(token); JWT.JWTInfo jwtResult = null; - for (JsonElement key : keys) { - JsonObject keyObject = key.getAsJsonObject(); - String kid = keyObject.get("kid").getAsString(); - if (jwtInfo.kid.equals(kid)) { - jwtResult = JWT.verifyJWTAndGetPayload(jwtInfo, keyObject.get("n").getAsString(), keyObject.get("e").getAsString()); - break; - } + + // Check if the key for the given kid exists in the cache + JsonObject keyObject = cachedKeys.get(jwtInfo.kid); + if (keyObject != null) { + jwtResult = JWT.verifyJWTAndGetPayload(jwtInfo, keyObject.get("n").getAsString(), keyObject.get("e").getAsString()); } + if (jwtResult == null) { - throw new RuntimeException("No matching key found for JWT verification"); + // If no matching key found and retry count is not exceeded, refetch the keys + if (retryCount < MAX_RETRIES) { + jwksCache.remove(jwksUrl); // Invalidate cache + return reSignToken(appIdentifier, main, token, iss, stt, useDynamicSigningKey, retryCount + 1); // Retry with incremented count + } else { + throw new OAuthAPIException("invalid_token", "The token is invalid or has expired.", 400); + } } + JsonObject payload = jwtResult.payload; // move keys in ext to root if (payload.has("ext")) { 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 eaef1b8b7..ee0dbfc71 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -18,6 +18,7 @@ import com.google.gson.*; import io.supertokens.Main; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.httpRequest.HttpResponseException; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.OAuth; @@ -91,7 +92,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I super.sendJsonResponse(200, errorResponse, resp); } catch (TenantOrAppNotFoundException | InvalidConfigException | HttpResponseException | - StorageQueryException | BadPermissionException e) { + StorageQueryException | BadPermissionException | FeatureNotEnabledException e) { throw new ServletException(e); } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 2d724abef..980089966 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -19,6 +19,7 @@ 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.OAuth; @@ -88,7 +89,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I errorResponse.addProperty("status", "OAUTH2_TOKEN_ERROR"); super.sendJsonResponse(200, errorResponse, resp); - } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { + } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException | + StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | + InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | + UnsupportedJWTSigningAlgorithmException | FeatureNotEnabledException e) { throw new ServletException(e); } } From 824cbbdf343ff2f8982f0335895620a6cb7667e1 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 27 Aug 2024 14:29:35 +0530 Subject: [PATCH 06/40] fix: exceptions --- src/main/java/io/supertokens/oauth/OAuth.java | 14 +++++++------- .../exceptions/OAuthAPIInvalidInputException.java | 4 ++-- .../exceptions/OAuthClientNotFoundException.java | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 7945fba5e..a0c6eb32b 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -81,9 +81,9 @@ private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) return; } } - throw new FeatureNotEnabledException( - "OAuth feature is not enabled. Please subscribe to a SuperTokens core license key to enable this " + - "feature."); + // throw new FeatureNotEnabledException( + // "OAuth feature is not enabled. Please subscribe to a SuperTokens core license key to enable this " + + // "feature."); } public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject paramsFromSdk, String inputCookies) @@ -138,7 +138,7 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app redirectTo = redirectTo.replace("code=ory_ac_", "code=st_ac_"); } } else { - throw new RuntimeException("Unexpected answer from Oauth Provider"); + throw new IllegalStateException("Unexpected answer from Oauth Provider"); } if(responseHeaders.containsKey(COOKIES_HEADER_NAME)){ cookies = new ArrayList<>(responseHeaders.get(COOKIES_HEADER_NAME)); @@ -324,7 +324,7 @@ public static JsonObject loadOAuthClient(Main main, AppIdentifier appIdentifier, String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException("Unable to locate the resource", "", 400); + throw new OAuthClientNotFoundException("invalid_client", "Invalid client_id specified"); } else { try { JsonObject hydraResponse = HttpRequest.sendGETRequest(main, "", adminOAuthProviderServiceUrl + HYDRA_CLIENTS_ENDPOINT + "/" + clientId, null, 10000, 10000, null); @@ -348,7 +348,7 @@ public static void deleteOAuthClient(Main main, AppIdentifier appIdentifier, Sto String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException("Unable to locate the resource", "", 400); + throw new OAuthClientNotFoundException("invalid_client", "Invalid client_id specified"); } else { try { oauthStorage.removeAppClientAssociation(appIdentifier, clientId); @@ -376,7 +376,7 @@ public static JsonObject updateOauthClient(Main main, AppIdentifier appIdentifie String clientId = paramsFromSdk.get("clientId").getAsString(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException("Unable to locate the resource", "", 400); + throw new OAuthClientNotFoundException("invalid_client", "Invalid client_id specified"); } else { JsonArray hydraInput = translateIncomingDataToHydraUpdateFormat(paramsFromSdk); try { diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java index 3f66d9f27..e84f89611 100644 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java @@ -23,7 +23,7 @@ public class OAuthAPIInvalidInputException extends OAuthException{ @Serial private static final long serialVersionUID = 665027786586190611L; - public OAuthAPIInvalidInputException(String error, String errorDescription, int statusCode) { - super(error, errorDescription, statusCode); + public OAuthAPIInvalidInputException(String error, String errorDescription) { + super(error, errorDescription, 400); } } diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java index c8697c9da..7e471c682 100644 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java @@ -22,7 +22,7 @@ public class OAuthClientNotFoundException extends OAuthException{ @Serial private static final long serialVersionUID = 1412853176388698991L; - public OAuthClientNotFoundException(String error, String errorDescription, int statusCode) { - super(error, errorDescription, statusCode); + public OAuthClientNotFoundException(String error, String errorDescription) { + super(error, errorDescription, 400); } } From cf408d4814837b674dd5f832b802015c0d3bd7ec Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 27 Aug 2024 14:41:06 +0530 Subject: [PATCH 07/40] fix: refactor --- src/main/java/io/supertokens/oauth/OAuth.java | 75 ++++++++++--------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index a0c6eb32b..929695727 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -109,45 +109,45 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { throw new OAuthAPIException("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.", 400); - } else { - // we query hydra - Map queryParamsForHydra = constructHydraRequestParamsForAuthorizationGETAPICall(paramsFromSdk); - Map headers = new HashMap<>(); - Map> responseHeaders = new HashMap<>(); + } - if (inputCookies != null) { - headers.put("Cookie", inputCookies); - } + // we query hydra + Map queryParamsForHydra = constructHydraRequestParamsForAuthorizationGETAPICall(paramsFromSdk); + Map headers = new HashMap<>(); + Map> responseHeaders = new HashMap<>(); - HttpRequest.sendGETRequestWithResponseHeaders(main, "", publicOAuthProviderServiceUrl + HYDRA_AUTH_ENDPOINT, queryParamsForHydra, headers, 10000, 10000, null, responseHeaders, false); + if (inputCookies != null) { + headers.put("Cookie", inputCookies); + } - 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 OAuthAPIException(error, errorDescription, 400); - } + HttpRequest.sendGETRequestWithResponseHeaders(main, "", publicOAuthProviderServiceUrl + HYDRA_AUTH_ENDPOINT, queryParamsForHydra, headers, 10000, 10000, null, responseHeaders, false); - if(Utils.containsUrl(locationHeaderValue, hydraBaseUrlForConsentAndLogin, true)){ - redirectTo = locationHeaderValue.replace(hydraBaseUrlForConsentAndLogin, "{apiDomain}"); - } else { - redirectTo = locationHeaderValue; - } - if (redirectTo.contains("code=ory_ac_")) { - redirectTo = redirectTo.replace("code=ory_ac_", "code=st_ac_"); - } + 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 OAuthAPIException(error, errorDescription, 400); + } + + if(Utils.containsUrl(locationHeaderValue, hydraBaseUrlForConsentAndLogin, true)){ + redirectTo = locationHeaderValue.replace(hydraBaseUrlForConsentAndLogin, "{apiDomain}"); } else { - throw new IllegalStateException("Unexpected answer from Oauth Provider"); + redirectTo = locationHeaderValue; + } + if (redirectTo.contains("code=ory_ac_")) { + redirectTo = redirectTo.replace("code=ory_ac_", "code=st_ac_"); } - if(responseHeaders.containsKey(COOKIES_HEADER_NAME)){ - cookies = new ArrayList<>(responseHeaders.get(COOKIES_HEADER_NAME)); + } else { + throw new IllegalStateException("Unexpected answer from Oauth Provider"); + } + if(responseHeaders.containsKey(COOKIES_HEADER_NAME)){ + cookies = new ArrayList<>(responseHeaders.get(COOKIES_HEADER_NAME)); - for (int i = 0; i < cookies.size(); i++) { - String cookieStr = cookies.get(i); - if (cookieStr.startsWith("ory_hydra_")) { - cookies.set(i, "st_oauth_" + cookieStr.substring(10)); - } + for (int i = 0; i < cookies.size(); i++) { + String cookieStr = cookies.get(i); + if (cookieStr.startsWith("ory_hydra_")) { + cookies.set(i, "st_oauth_" + cookieStr.substring(10)); } } } @@ -206,7 +206,7 @@ public static JsonObject getToken(Main main, AppIdentifier appIdentifier, Storag } } - private static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, int stt, boolean useDynamicSigningKey, int retryCount) throws IOException, HttpResponseException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException, OAuthAPIException { + private static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, int stt, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException, OAuthAPIException { // Load the JWKS from the specified URL String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); String jwksUrl = publicOAuthProviderServiceUrl + HYDRA_JWKS_PATH; // Use the new constant @@ -214,10 +214,15 @@ private static String reSignToken(AppIdentifier appIdentifier, Main main, String // Check if cached keys are available for the jwksUrl Map cachedKeys = jwksCache.get(jwksUrl); if (cachedKeys == null) { - JsonObject jwksResponse = HttpRequest.sendGETRequest(main, "", jwksUrl, null, 10000, 10000, null); + JsonObject jwksResponse; + try { + jwksResponse = HttpRequest.sendGETRequest(main, "", jwksUrl, null, 10000, 10000, null); + } catch (HttpResponseException e) { + throw new OAuthAPIException("jwks_error", "Could not fetch JWKS keys from hydra for token verification", 500); + } cachedKeys = new HashMap<>(); JsonArray keysArray = jwksResponse.get("keys").getAsJsonArray(); - + // Populate the cache with keys indexed by kid for (JsonElement key : keysArray) { JsonObject keyObject = key.getAsJsonObject(); From 802bbd3a46667d343cd4bff5be2e32ba38fd9f8f Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 28 Aug 2024 18:03:36 +0530 Subject: [PATCH 08/40] fix: refactor --- src/main/java/io/supertokens/oauth/OAuth.java | 321 ++++--- .../io/supertokens/oauth/Transformations.java | 170 ++++ src/main/java/io/supertokens/oauth/Utils.java | 25 + .../oauth/exceptions/OAuthAPIException.java | 18 +- .../OAuthClientNotFoundException.java | 6 +- .../io/supertokens/webserver/Webserver.java | 4 +- ...a => CreateUpdateOrGetOAuthClientAPI.java} | 10 +- .../webserver/api/oauth/OAuthAuthAPI.java | 102 ++- .../webserver/api/oauth/OAuthProxyBase.java | 165 ++++ .../webserver/api/oauth/OAuthTokenAPI.java | 70 +- .../test/oauth/api/OAuthAuthAPITest.java | 492 +++++----- .../test/oauth/api/OAuthClientsAPITest.java | 851 +++++++++--------- 12 files changed, 1323 insertions(+), 911 deletions(-) create mode 100644 src/main/java/io/supertokens/oauth/Transformations.java create mode 100644 src/main/java/io/supertokens/oauth/Utils.java rename src/main/java/io/supertokens/webserver/api/oauth/{OAuthClientsAPI.java => CreateUpdateOrGetOAuthClientAPI.java} (95%) create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 929695727..79488f234 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -48,25 +48,26 @@ import io.supertokens.signingkeys.SigningKeys; import io.supertokens.utils.Utils; +import java.io.BufferedReader; +import java.io.DataOutputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.lang.reflect.InvocationTargetException; -import java.net.URLDecoder; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.util.*; +import java.util.stream.Collectors; public class OAuth { + private static final int CONNECTION_TIMEOUT = 5000; + private static final int READ_TIMEOUT = 5000; - 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="; - - private static final String HYDRA_AUTH_ENDPOINT = "/oauth2/auth"; - private static final String HYDRA_TOKEN_ENDPOINT = "/oauth2/token"; private static final String HYDRA_CLIENTS_ENDPOINT = "/admin/clients"; private static final String HYDRA_JWKS_PATH = "/.well-known/jwks.json"; // New constant for JWKS path @@ -81,135 +82,106 @@ private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) return; } } - // throw new FeatureNotEnabledException( - // "OAuth feature is not enabled. Please subscribe to a SuperTokens core license key to enable this " + - // "feature."); - } - public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject paramsFromSdk, String inputCookies) - throws InvalidConfigException, HttpResponseException, IOException, OAuthAPIException, StorageQueryException, - TenantOrAppNotFoundException, FeatureNotEnabledException { + throw new FeatureNotEnabledException( + "OAuth feature is not enabled. Please subscribe to a SuperTokens core license key to enable this " + + "feature."); + } + public static Response handleOAuthProxyGET(Main main, AppIdentifier appIdentifier, Storage storage, String path, Map queryParams, 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 (queryParams.containsKey("client_id")) { + String clientId = queryParams.get("client_id"); + if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { + throw new OAuthClientNotFoundException(); + } + } + + // Request transformations + queryParams = Transformations.transformQueryParamsForHydra(queryParams); + headers = Transformations.transformRequestHeadersForHydra(headers); String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); - String hydraInternalAddress = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOauthProviderUrlConfiguredInHydra(); - String hydraBaseUrlForConsentAndLogin = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOauthProviderConsentLoginBaseUrl(); + String fullUrl = publicOAuthProviderServiceUrl + path; - String clientId = paramsFromSdk.get("client_id").getAsString(); + Response response = doGet(fullUrl, headers, queryParams); - if (inputCookies != null) { - inputCookies = inputCookies.replaceAll("st_oauth_", "ory_hydra_"); - } + // Response transformations + response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); + response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthAPIException("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.", 400); - } + checkNonSuccessResponse(response); + + return response; + } - // we query hydra - Map queryParamsForHydra = constructHydraRequestParamsForAuthorizationGETAPICall(paramsFromSdk); - Map headers = new HashMap<>(); - Map> responseHeaders = new HashMap<>(); + public static Response handleOAuthProxyFormPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, Map formFields, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + checkForOauthFeature(appIdentifier, main); + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - if (inputCookies != null) { - headers.put("Cookie", inputCookies); + if (formFields.containsKey("client_id")) { + String clientId = formFields.get("client_id"); + if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { + throw new OAuthClientNotFoundException(); + } } - HttpRequest.sendGETRequestWithResponseHeaders(main, "", publicOAuthProviderServiceUrl + HYDRA_AUTH_ENDPOINT, queryParamsForHydra, headers, 10000, 10000, null, responseHeaders, false); + // Request transformations + formFields = Transformations.transformFormFieldsForHydra(formFields); + headers = Transformations.transformRequestHeadersForHydra(headers); - 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 OAuthAPIException(error, errorDescription, 400); - } + String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); + String fullUrl = publicOAuthProviderServiceUrl + path; - if(Utils.containsUrl(locationHeaderValue, hydraBaseUrlForConsentAndLogin, true)){ - redirectTo = locationHeaderValue.replace(hydraBaseUrlForConsentAndLogin, "{apiDomain}"); - } else { - redirectTo = locationHeaderValue; - } - if (redirectTo.contains("code=ory_ac_")) { - redirectTo = redirectTo.replace("code=ory_ac_", "code=st_ac_"); - } - } else { - throw new IllegalStateException("Unexpected answer from Oauth Provider"); - } - if(responseHeaders.containsKey(COOKIES_HEADER_NAME)){ - cookies = new ArrayList<>(responseHeaders.get(COOKIES_HEADER_NAME)); + Response response = doFormPost(fullUrl, headers, formFields); - for (int i = 0; i < cookies.size(); i++) { - String cookieStr = cookies.get(i); - if (cookieStr.startsWith("ory_hydra_")) { - cookies.set(i, "st_oauth_" + cookieStr.substring(10)); - } - } - } + // Response transformations + response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); + response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); - return new OAuthAuthResponse(redirectTo, cookies); - } + checkNonSuccessResponse(response); - public static JsonObject getToken(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject bodyFromSdk, String iss, boolean useDynamicKey) throws InvalidConfigException, TenantOrAppNotFoundException, OAuthAPIException, StorageQueryException, IOException, InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, JWTCreationException, JWTException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, FeatureNotEnabledException { - checkForOauthFeature(appIdentifier, main); - - OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - String clientId = bodyFromSdk.get("client_id").getAsString(); + return response; + } - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthAPIException("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.", 400); + private static void checkNonSuccessResponse(Response response) throws OAuthAPIException { + if (response.statusCode >= 400) { + String error = response.jsonResponse.get("error").getAsString(); + String errorDebug = response.jsonResponse.get("error_debug").getAsString(); + String errorDescription = response.jsonResponse.get("error_description").getAsString(); + String errorHint = response.jsonResponse.get("error_hint").getAsString(); + throw new OAuthAPIException(error, errorDebug, errorDescription, errorHint, response.statusCode); } + } - String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); - try { - Map bodyParams = constructHydraRequestParamsForAuthorizationGETAPICall(bodyFromSdk); - if (bodyParams.containsKey("code")) { - bodyParams.put("code", bodyParams.get("code").replace("st_ac_", "ory_ac_")); - } - if (bodyParams.containsKey("refresh_token")) { - bodyParams.put("refresh_token", bodyParams.get("refresh_token").replace("st_rt_", "ory_rt_")); - } - JsonObject response = HttpRequest.sendFormPOSTRequest(main, "", publicOAuthProviderServiceUrl + HYDRA_TOKEN_ENDPOINT, bodyParams, 10000, 10000, null); - - // token transformations - if (response.has("access_token")) { - String accessToken = response.get("access_token").getAsString(); - accessToken = reSignToken(appIdentifier, main, accessToken, iss, 1, useDynamicKey, 0); - response.addProperty("access_token", accessToken); - } + public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject jsonBody, String iss, boolean useDynamicKey) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { + if (jsonBody.has("access_token")) { + String accessToken = jsonBody.get("access_token").getAsString(); + accessToken = reSignToken(appIdentifier, main, accessToken, iss, 1, useDynamicKey, 0); + jsonBody.addProperty("access_token", accessToken); + } - if (response.has("id_token")) { - String idToken = response.get("id_token").getAsString(); - idToken = reSignToken(appIdentifier, main, idToken, iss, 2, useDynamicKey, 0); - response.addProperty("id_token", idToken); - } + if (jsonBody.has("id_token")) { + String idToken = jsonBody.get("id_token").getAsString(); + idToken = reSignToken(appIdentifier, main, idToken, iss, 2, useDynamicKey, 0); + jsonBody.addProperty("id_token", idToken); + } - if (response.has("refresh_token")) { - String refreshToken = response.get("refresh_token").getAsString(); - refreshToken = refreshToken.replace("ory_rt_", "st_rt_"); - response.addProperty("refresh_token", refreshToken); - } - return response; - - } catch (HttpResponseException e) { - JsonObject errorResponse = new Gson().fromJson(e.rawMessage, JsonObject.class); - throw new OAuthAPIException( - errorResponse.get("error").getAsString(), - errorResponse.get("error_description").getAsString(), - e.statusCode - ); + if (jsonBody.has("refresh_token")) { + String refreshToken = jsonBody.get("refresh_token").getAsString(); + refreshToken = refreshToken.replace("ory_rt_", "st_rt_"); + jsonBody.addProperty("refresh_token", refreshToken); } + + return jsonBody; } - private static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, int stt, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException, OAuthAPIException { + private static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, int stt, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { // Load the JWKS from the specified URL String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); - String jwksUrl = publicOAuthProviderServiceUrl + HYDRA_JWKS_PATH; // Use the new constant + String jwksUrl = publicOAuthProviderServiceUrl + HYDRA_JWKS_PATH; // Check if cached keys are available for the jwksUrl Map cachedKeys = jwksCache.get(jwksUrl); @@ -218,7 +190,7 @@ private static String reSignToken(AppIdentifier appIdentifier, Main main, String try { jwksResponse = HttpRequest.sendGETRequest(main, "", jwksUrl, null, 10000, 10000, null); } catch (HttpResponseException e) { - throw new OAuthAPIException("jwks_error", "Could not fetch JWKS keys from hydra for token verification", 500); + throw new RuntimeException("Could not fetch JWKS keys from hydra for token verification"); } cachedKeys = new HashMap<>(); JsonArray keysArray = jwksResponse.get("keys").getAsJsonArray(); @@ -248,7 +220,7 @@ private static String reSignToken(AppIdentifier appIdentifier, Main main, String jwksCache.remove(jwksUrl); // Invalidate cache return reSignToken(appIdentifier, main, token, iss, stt, useDynamicSigningKey, retryCount + 1); // Retry with incremented count } else { - throw new OAuthAPIException("invalid_token", "The token is invalid or has expired.", 400); + throw new RuntimeException("Could not verify token with hydra"); } } @@ -329,18 +301,14 @@ public static JsonObject loadOAuthClient(Main main, AppIdentifier appIdentifier, String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException("invalid_client", "Invalid client_id specified"); + throw new OAuthClientNotFoundException(); } 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); - } +// throw createCustomExceptionFromHttpResponseException(e, OAuthException.class); + throw new IllegalStateException("FIXME"); // TODO fixme } } } @@ -353,18 +321,19 @@ public static void deleteOAuthClient(Main main, AppIdentifier appIdentifier, Sto String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException("invalid_client", "Invalid client_id specified"); + throw new OAuthClientNotFoundException(); } 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); - } +// try { +// throw createCustomExceptionFromHttpResponseException(e, OAuthException.class); +// } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | +// IllegalAccessException ex) { +// throw new RuntimeException(ex); +// } + throw new IllegalStateException("FIXME"); // TODO fixme } } } @@ -381,7 +350,7 @@ public static JsonObject updateOauthClient(Main main, AppIdentifier appIdentifie String clientId = paramsFromSdk.get("clientId").getAsString(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException("invalid_client", "Invalid client_id specified"); + throw new OAuthClientNotFoundException(); } else { JsonArray hydraInput = translateIncomingDataToHydraUpdateFormat(paramsFromSdk); try { @@ -393,7 +362,7 @@ public static JsonObject updateOauthClient(Main main, AppIdentifier appIdentifie int responseStatusCode = e.statusCode; switch (responseStatusCode){ case 400 -> throw createCustomExceptionFromHttpResponseException(e, OAuthAPIInvalidInputException.class); - case 404 -> throw createCustomExceptionFromHttpResponseException(e, OAuthClientNotFoundException.class); + case 404 -> throw new OAuthClientNotFoundException(); case 500 -> throw createCustomExceptionFromHttpResponseException(e, OAuthClientUpdateException.class); // hydra is not so helpful with the error messages at this endpoint.. default -> throw new RuntimeException(e); } @@ -463,25 +432,93 @@ private static JsonObject formatResponseForSDK(JsonObject response) { return formattedResponse; } - private static Map constructHydraRequestParamsForAuthorizationGETAPICall(JsonObject inputFromSdk) { - Map queryParamsForHydra = new HashMap<>(); - for(Map.Entry jsonElement : inputFromSdk.entrySet()){ - queryParamsForHydra.put(jsonElement.getKey(), jsonElement.getValue().getAsString()); + + public static Map convertCamelToSnakeCase(Map queryParams) { + Map result = new HashMap<>(); + for (Map.Entry entry : queryParams.entrySet()) { + result.put(Utils.camelCaseToSnakeCase(entry.getKey()), entry.getValue()); + } + return result; + } + + // HTTP Methods + + private static Response doGet(String url, Map headers, Map queryParams) throws IOException { + 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 queryParamsForHydra; + int responseCode = con.getResponseCode(); + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + String inputLine; + StringBuffer response = new StringBuffer(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + JsonObject jsonResponse = null; + if (con.getContentType() != null && con.getContentType().contains("application/json")) { + Gson gson = new Gson(); + jsonResponse = gson.fromJson(response.toString(), JsonObject.class); + } + return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); } - private static String getValueOfQueryParam(String url, String queryParam){ - String valueOfQueryParam = ""; - if(!queryParam.endsWith("=")){ - queryParam = queryParam + "="; + private static Response doFormPost(String url, Map headers, Map formFields) throws IOException { + 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("&"))); + } + int responseCode = con.getResponseCode(); + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + String inputLine; + StringBuffer response = new StringBuffer(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); } - int startIndex = url.indexOf(queryParam) + queryParam.length(); // start after the '=' sign - int endIndex = url.indexOf("&", startIndex); - if (endIndex == -1){ - endIndex = url.length(); + in.close(); + JsonObject jsonResponse = null; + if (con.getContentType().contains("application/json")) { + Gson gson = new Gson(); + jsonResponse = gson.fromJson(response.toString(), JsonObject.class); + } + return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); + } + public static class Response { + public int statusCode; + public String rawResponse; + public JsonObject jsonResponse; + public Map> headers; + + public Response(int statusCode, String rawResponse, JsonObject jsonResponse, Map> headers) { + this.statusCode = statusCode; + this.rawResponse = rawResponse; + this.jsonResponse = jsonResponse; + this.headers = headers; } - 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); } -;} +} 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..231962c8f --- /dev/null +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -0,0 +1,170 @@ +package io.supertokens.oauth; + +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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 { + + public static Map transformQueryParamsForHydra(Map queryParams) { + return queryParams; + } + + 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; + } + + public static String transformRedirectUrlFromHydra(String redirectTo) { + try { + URL url = new URL(redirectTo); + String query = url.getQuery(); + String[] queryParams = query.split("&"); + StringBuilder updatedQuery = new StringBuilder(); + for (String param : queryParams) { + String[] keyValue = param.split("="); + if (keyValue.length > 1 && keyValue[1].startsWith("ory_")) { + String decodedValue = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8.name()); + if (decodedValue.startsWith("ory_")) { + decodedValue = decodedValue.replaceFirst("ory_", "st_"); + } + String encodedValue = URLEncoder.encode(decodedValue, StandardCharsets.UTF_8.name()); + updatedQuery.append(keyValue[0]).append("=").append(encodedValue).append("&"); + } else { + updatedQuery.append(param).append("&"); + } + } + redirectTo = url.getProtocol() + "://" + url.getHost() + ":" + url.getPort() + url.getPath() + "?" + + updatedQuery.toString().trim(); + } catch (MalformedURLException | UnsupportedEncodingException 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_")) { + 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 JsonObject transformJsonResponseFromHydra(JsonObject jsonResponse) { + return jsonResponse; + } + + 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 + + // Location transformation + final String LOCATION_HEADER_NAME = "Location"; + + if (headers.containsKey(LOCATION_HEADER_NAME)) { + String hydraInternalAddress = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main) + .getOauthProviderUrlConfiguredInHydra(); + String hydraBaseUrlForConsentAndLogin = Config + .getConfig(appIdentifier.getAsPublicTenantIdentifier(), main) + .getOauthProviderConsentLoginBaseUrl(); + + String redirectTo = headers.get(LOCATION_HEADER_NAME).get(0); + + 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), pair.substring(idx + 1)); + } + } + String error = urlQueryParams.getOrDefault("error", null); + String errorDebug = urlQueryParams.getOrDefault("error_debug", null); + String errorDescription = urlQueryParams.getOrDefault("error_description", null); + String errorHint = urlQueryParams.getOrDefault("error_hint", null); + throw new OAuthAPIException(error, errorDebug, errorDescription, errorHint, 400); + + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + } + + if (Utils.containsUrl(redirectTo, hydraBaseUrlForConsentAndLogin, true)) { + redirectTo = redirectTo.replace(hydraBaseUrlForConsentAndLogin, "{apiDomain}"); + } + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + + redirectTo = transformRedirectUrlFromHydra(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; + } +} diff --git a/src/main/java/io/supertokens/oauth/Utils.java b/src/main/java/io/supertokens/oauth/Utils.java new file mode 100644 index 000000000..ee2fb9fbf --- /dev/null +++ b/src/main/java/io/supertokens/oauth/Utils.java @@ -0,0 +1,25 @@ +package io.supertokens.oauth; + +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +public class Utils { + public static Map getQueryParamsMapFromJsonObject(JsonObject jsonObject) { + Map queryParams = new HashMap<>(); + for (Map.Entry entry : jsonObject.entrySet()) { + queryParams.put(entry.getKey(), entry.getValue().getAsString()); + } + return queryParams; + } + + public static Map getBodyParamsMapFromJsonObject(JsonObject bodyFromSdk) { + Map bodyParams = new HashMap<>(); + for (Map.Entry entry : bodyFromSdk.entrySet()) { + bodyParams.put(entry.getKey(), entry.getValue().getAsString()); + } + return bodyParams; + } +} diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java index 6cfb5a523..a2cba83f1 100644 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java @@ -16,10 +16,22 @@ package io.supertokens.oauth.exceptions; -public class OAuthAPIException extends OAuthException{ +public class OAuthAPIException extends Exception { private static final long serialVersionUID = 1836718299845759897L; - public OAuthAPIException(String error, String errorDescription, int statusCode) { - super(error, errorDescription, statusCode); + public final String error; + public final String errorDebug; + public final String errorDescription; + public final String errorHint; + public final int statusCode; + + public OAuthAPIException(String error, String errorDebug, String errorDescription, String errorHint, int statusCode) { + super(error); + + this.error = error; + this.errorDebug = errorDebug; + this.errorDescription = errorDescription; + this.errorHint = errorHint; + this.statusCode = statusCode; } } diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthClientNotFoundException.java index 7e471c682..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, 400); - } } diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index fe3f9c3ec..5a19ed282 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -40,7 +40,7 @@ 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.CreateUpdateOrGetOAuthClientAPI; import io.supertokens.webserver.api.oauth.OAuthTokenAPI; import io.supertokens.webserver.api.passwordless.*; import io.supertokens.webserver.api.session.*; @@ -272,7 +272,7 @@ private void setupRoutes() { addAPI(new OAuthAuthAPI(main)); addAPI(new OAuthTokenAPI(main)); - addAPI(new OAuthClientsAPI(main)); + addAPI(new CreateUpdateOrGetOAuthClientAPI(main)); StandardContext context = tomcatReference.getContext(); Tomcat tomcat = tomcatReference.getTomcat(); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientsAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java similarity index 95% rename from src/main/java/io/supertokens/webserver/api/oauth/OAuthClientsAPI.java rename to src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index a0c269ec1..aa0795238 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientsAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -43,7 +43,7 @@ import java.util.Arrays; import java.util.List; -public class OAuthClientsAPI extends WebserverAPI { +public class CreateUpdateOrGetOAuthClientAPI extends WebserverAPI { @Serial private static final long serialVersionUID = -4482427281337641246L; @@ -57,7 +57,7 @@ public class OAuthClientsAPI extends WebserverAPI { public String getPath() { return "/recipe/oauth/clients"; } - public OAuthClientsAPI(Main main){ + public CreateUpdateOrGetOAuthClientAPI(Main main){ super(main, RECIPE_ID.OAUTH.toString()); } @@ -103,7 +103,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO sendJsonResponse(200, response, resp); } catch (OAuthClientNotFoundException e) { - JsonObject errorResponse = createJsonFromException(e, OAUTH2_CLIENT_NOT_FOUND_ERROR); + JsonObject errorResponse = new JsonObject(); sendJsonResponse(200, errorResponse, resp); } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException @@ -128,7 +128,7 @@ protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws sendJsonResponse(200, responseBody, resp); } catch (OAuthClientNotFoundException e) { - JsonObject errorResponse = createJsonFromException(e, OAUTH2_CLIENT_NOT_FOUND_ERROR); + JsonObject errorResponse = new JsonObject(); sendJsonResponse(200, errorResponse, resp); } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException @@ -159,7 +159,7 @@ protected void doPatch(HttpServletRequest req, HttpServletResponse resp) throws throw new ServletException(updateException); } catch (OAuthClientNotFoundException clientNotFoundException) { - JsonObject errorResponse = createJsonFromException(clientNotFoundException, OAUTH2_CLIENT_NOT_FOUND_ERROR); + JsonObject errorResponse = new JsonObject(); sendJsonResponse(200, errorResponse, resp); } catch (TenantOrAppNotFoundException | InvalidConfigException | StorageQueryException | 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 ee0dbfc71..bbba9f608 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -16,84 +16,86 @@ 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.featureflag.exceptions.FeatureNotEnabledException; -import io.supertokens.httpRequest.HttpResponseException; -import io.supertokens.multitenancy.exception.BadPermissionException; -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.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; import jakarta.servlet.http.HttpServletRequest; 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 class OAuthAuthAPI extends OAuthProxyBase { public OAuthAuthAPI(Main main) { - super(main, RECIPE_ID.OAUTH.toString()); + super(main); } - private static final List REQUIRED_FIELDS_FOR_POST = Arrays.asList(new String[]{"client_id", "response_type"}); - @Override public String getPath() { return "/recipe/oauth/auth"; } @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + public OAuthProxyBase.ProxyProps getProxyProperties() { + return new OAuthProxyBase.ProxyProps( + new String[]{ "POST" }, // apiMethods + "GET", // method + "/oauth2/auth", // path + false // camelToSnakeCaseConversion + ); + } - JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + @Override + protected Map getQueryParamsForProxy(HttpServletRequest req, JsonObject input) throws IOException, ServletException { JsonObject params = InputParser.parseJsonObjectOrThrowError(input, "params", false); + + return params.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue().getAsString() + )); + } + + @Override + protected Map getHeadersForProxy(HttpServletRequest req, JsonObject input) throws ServletException, IOException { String cookies = InputParser.parseStringOrThrowError(input, "cookies", true); - InputParser.throwErrorOnMissingRequiredField(params, REQUIRED_FIELDS_FOR_POST); + Map headers = new HashMap<>(); - try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + if (cookies != null) { + headers.put("Cookie", cookies); + } - OAuthAuthResponse authResponse = OAuth.getAuthorizationUrl(super.main, appIdentifier, storage, params, cookies); - JsonObject response = new JsonObject(); - response.addProperty("redirectTo", authResponse.redirectTo); + return headers; + } - JsonArray jsonCookies = new JsonArray(); - if (authResponse.cookies != null) { - for(String cookie : authResponse.cookies){ - jsonCookies.add(new JsonPrimitive(cookie)); - } - } - response.add("cookies", jsonCookies); - response.addProperty("status", "OK"); - super.sendJsonResponse(200, response, resp); + @Override + protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + if (headers == null || !headers.containsKey("Location")) { + throw new IllegalStateException("Invalid response from hydra"); + } - } catch (OAuthAPIException authException) { + String redirectTo = headers.get("Location").get(0); + List cookies = 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 response = new JsonObject(); + response.addProperty("redirectTo", redirectTo); - } catch (TenantOrAppNotFoundException | InvalidConfigException | HttpResponseException | - StorageQueryException | BadPermissionException | FeatureNotEnabledException e) { - throw new ServletException(e); + JsonArray jsonCookies = new JsonArray(); + if (cookies != null) { + for (String cookie : cookies) { + jsonCookies.add(new JsonPrimitive(cookie)); + } } + + response.add("cookies", jsonCookies); + response.addProperty("status", "OK"); + super.sendJsonResponse(200, response, resp); } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java new file mode 100644 index 000000000..2944acaab --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java @@ -0,0 +1,165 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.io.Serial; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +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.OAuth; +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 abstract class OAuthProxyBase extends WebserverAPI { + @Serial + private static final long serialVersionUID = -8734479943734920904L; + + public OAuthProxyBase(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + public abstract ProxyProps getProxyProperties(); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + ProxyProps proxyProps = getProxyProperties(); + if (!Arrays.asList(proxyProps.apiMethods).contains(req.getMethod())) { + this.sendTextResponse(405, "Method not supported", resp); + return; + } + + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + if (proxyProps.method == "GET") { + doProxyGetRequest(req, resp, proxyProps, input); + } else if (proxyProps.method == "POST_FORM") { + doProxyPostFormRequest(req, resp, proxyProps, input); + } + } + + private void doProxyGetRequest(HttpServletRequest req, HttpServletResponse resp, ProxyProps proxyProps, JsonObject input) + throws IOException, ServletException { + Map queryParams = getQueryParamsForProxy(req, input); + + if (proxyProps.camelToSnakeCaseConversion) { + queryParams = OAuth.convertCamelToSnakeCase(queryParams); + } + + Map headers = getHeadersForProxy(req, input); + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + OAuth.Response response = OAuth.handleOAuthProxyGET(main, appIdentifier, storage, proxyProps.path, queryParams, headers); + + handleResponseFromProxyGET(req, resp, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); + + } catch (OAuthClientNotFoundException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); + + this.sendJsonResponse(400, response, resp); + + } catch (OAuthAPIException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "OAUTH_ERROR"); + response.addProperty("error", e.error); + response.addProperty("error_debug", e.errorDebug); + response.addProperty("error_description", e.errorDescription); + response.addProperty("error_hint", e.errorHint); + response.addProperty("status_code", e.statusCode); + this.sendJsonResponse(200, response, resp); + + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { + throw new ServletException(e); + } + } + + private void doProxyPostFormRequest(HttpServletRequest req, HttpServletResponse resp, ProxyProps proxyProps, JsonObject input) + throws IOException, ServletException { + Map formFields = getFormFieldsForProxyPOST(req, input); + + if (proxyProps.camelToSnakeCaseConversion) { + formFields = OAuth.convertCamelToSnakeCase(formFields); + } + + Map headers = getHeadersForProxy(req, input); + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + OAuth.Response response = OAuth.handleOAuthProxyFormPOST(main, appIdentifier, storage, proxyProps.path, formFields, headers); + + handleResponseFromProxyPOST(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); + + } catch (OAuthClientNotFoundException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); + + this.sendJsonResponse(400, response, resp); + + } catch (OAuthAPIException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "OAUTH_ERROR"); + response.addProperty("error", e.error); + response.addProperty("error_debug", e.errorDebug); + response.addProperty("error_description", e.errorDescription); + response.addProperty("error_hint", e.errorHint); + response.addProperty("status_code", e.statusCode); + this.sendJsonResponse(200, response, resp); + + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { + throw new ServletException(e); + } + } + + protected Map getQueryParamsForProxy(HttpServletRequest req, JsonObject input) throws IOException, ServletException { + return null; + } + + protected Map getHeadersForProxy(HttpServletRequest req, JsonObject input) throws IOException, ServletException { + return null; + } + + protected Map getFormFieldsForProxyPOST(HttpServletRequest req, JsonObject input) throws IOException, ServletException { + return null; + } + + protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + throw new IllegalStateException("Not implemented"); + } + + protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + throw new IllegalStateException("Not implemented"); + } + + public static class ProxyProps { + public final String[] apiMethods; + public final String method; + public final String path; + public final boolean camelToSnakeCaseConversion; + + public ProxyProps(String[] apiMethods, String method, String path, boolean camelToSnakeCaseConversion) { + this.apiMethods = apiMethods; + this.method = method; + this.path = path; + this.camelToSnakeCaseConversion = camelToSnakeCaseConversion; + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 980089966..68d6f34a4 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -19,12 +19,9 @@ 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.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; @@ -39,17 +36,17 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import java.io.Serial; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; -public class OAuthTokenAPI extends WebserverAPI { - @Serial - private static final long serialVersionUID = -8734479943734920904L; +public class OAuthTokenAPI extends OAuthProxyBase { public OAuthTokenAPI(Main main) { - super(main, RECIPE_ID.OAUTH.toString()); + super(main); } @Override @@ -58,42 +55,51 @@ public String getPath() { } @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + public ProxyProps getProxyProperties() { + return new ProxyProps( + new String[] { "POST" }, // apiMethods + "POST_FORM", // method + "/oauth2/token", // path + false // camelToSnakeCaseConversion + ); + } - JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - String iss = InputParser.parseStringOrThrowError(input, "iss", false); + @Override + protected Map getFormFieldsForProxyPOST(HttpServletRequest req, JsonObject input) throws IOException, ServletException { + InputParser.parseStringOrThrowError(input, "iss", false); // input validation + + JsonObject bodyFromSDK = InputParser.parseJsonObjectOrThrowError(input, "body", false); + + Map formFields = new HashMap<>(); + for (Map.Entry entry : bodyFromSDK.entrySet()) { + formFields.put(entry.getKey(), entry.getValue().getAsString()); + } + + return formFields; + } + + @Override + protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + if (jsonBody == null) { + throw new IllegalStateException("unexpected response from hydra"); + } + String iss = InputParser.parseStringOrThrowError(input, "iss", false); boolean useDynamicKey = false; Boolean useStaticKeyInput = InputParser.parseBooleanOrThrowError(input, "useStaticSigningKey", true); // useStaticKeyInput defaults to true, so we check if it has been explicitly set to false useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); - JsonObject bodyFromSDK = InputParser.parseJsonObjectOrThrowError(input, "body", false); - try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + jsonBody = OAuth.transformTokens(super.main, appIdentifier, storage, jsonBody, iss, useDynamicKey); - JsonObject response = OAuth.getToken(super.main, appIdentifier, storage, - bodyFromSDK, iss, useDynamicKey); - - response.addProperty("status", "OK"); - super.sendJsonResponse(200, response, resp); - - } catch (OAuthAPIException authException) { - - JsonObject errorResponse = new JsonObject(); - errorResponse.addProperty("error", authException.error); - errorResponse.addProperty("error_description", authException.errorDescription); - errorResponse.addProperty("status_code", authException.statusCode); - errorResponse.addProperty("status", "OAUTH2_TOKEN_ERROR"); - super.sendJsonResponse(200, errorResponse, resp); - - } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException | - StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | - InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | - UnsupportedJWTSigningAlgorithmException | FeatureNotEnabledException e) { + } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { throw new ServletException(e); } + + jsonBody.addProperty("status", "OK"); + super.sendJsonResponse(200, jsonBody, resp); } } 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 e0ccb9d6b..bdc2e5ee1 100644 --- a/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java +++ b/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java @@ -41,250 +41,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, 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)); - } +// 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..c667ce72d 100644 --- a/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java +++ b/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java @@ -43,430 +43,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)); +// } +// } From 7839b691e64e59ffa068b108a5b014eea5114fd5 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 29 Aug 2024 18:43:37 +0530 Subject: [PATCH 09/40] fix: refactor and client crud APIs --- build.gradle | 3 + .../io/supertokens/oauth/JWTVerification.java | 88 ++++ src/main/java/io/supertokens/oauth/OAuth.java | 445 ++++++++++++------ .../io/supertokens/oauth/Transformations.java | 76 +-- .../java/io/supertokens/session/jwt/JWT.java | 14 - src/main/java/io/supertokens/utils/Utils.java | 25 - .../io/supertokens/webserver/Webserver.java | 2 + .../CreateUpdateOrGetOAuthClientAPI.java | 200 ++++---- .../webserver/api/oauth/OAuthAuthAPI.java | 17 +- .../webserver/api/oauth/OAuthProxyBase.java | 242 +++++++++- .../webserver/api/oauth/OAuthTokenAPI.java | 17 +- .../api/oauth/RemoveOAuthClientAPI.java | 81 ++++ 12 files changed, 846 insertions(+), 364 deletions(-) create mode 100644 src/main/java/io/supertokens/oauth/JWTVerification.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java diff --git a/build.gradle b/build.gradle index b2f1a910d..ace25d503 100644 --- a/build.gradle +++ b/build.gradle @@ -73,6 +73,9 @@ dependencies { // https://mvnrepository.com/artifact/com.googlecode.libphonenumber/libphonenumber/ implementation group: 'com.googlecode.libphonenumber', name: 'libphonenumber', version: '8.13.25' + implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.12.6' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.12.6' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.12.6' compileOnly project(":supertokens-plugin-interface") testImplementation project(":supertokens-plugin-interface") diff --git a/src/main/java/io/supertokens/oauth/JWTVerification.java b/src/main/java/io/supertokens/oauth/JWTVerification.java new file mode 100644 index 000000000..08791d213 --- /dev/null +++ b/src/main/java/io/supertokens/oauth/JWTVerification.java @@ -0,0 +1,88 @@ +package io.supertokens.oauth; + +import java.security.Key; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.Locator; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.Jwks; +import io.supertokens.Main; +import io.supertokens.httpRequest.HttpRequest; + +public class JWTVerification { + + public static JsonObject verifyJWTAndGetPayload(Main main, String token, String jwksUrl) { + JwtParser jwtParser = Jwts.parser().keyLocator(new KeyLocatorImpl(main, jwksUrl)).build(); + Jws jwtResult = jwtParser.parseSignedClaims(token); + if (jwtResult == null) { + throw new RuntimeException("Failed to verify JWT token"); + } + Claims payload = jwtResult.getPayload(); + + return new Gson().fromJson(new Gson().toJson(payload), JsonObject.class); + } + + private static class KeyLocatorImpl implements Locator { + private static final int CONNECTION_TIMEOUT = 5000; + private static final int READ_TIMEOUT = 5000; + private static final int MAX_RETRIES = 3; // Maximum number of retries for fetching JWKS + + private static Map jwksCache = new HashMap<>(); // Cache for JWKS keys + + private final Main main; + private final String jwksUrl; + + public KeyLocatorImpl(Main main, String jwksUrl) { + this.main = main; + this.jwksUrl = jwksUrl; + } + + @Override + public Key locate(Header header) { + for (int i = 0; i < MAX_RETRIES; i++) { + JsonObject jwksResponse = jwksCache.get(jwksUrl); + if (jwksResponse == null) { + try { + jwksResponse = HttpRequest + .sendGETRequest(main, "", jwksUrl, null, CONNECTION_TIMEOUT, READ_TIMEOUT, null); + jwksCache.put(jwksUrl, jwksResponse); // Cache the fetched JWKS response + } catch (Exception e) { + throw new RuntimeException("Failed to fetch JWKS keys for token verification", e); + } + } + + Jwk jwk = null; + + JsonArray keys = jwksResponse.get("keys").getAsJsonArray(); + + for (JsonElement keyElement : keys) { + JsonObject keyObject = keyElement.getAsJsonObject(); + if (keyObject.get("kid").getAsString().equals(header.get("kid"))) { + jwk = Jwks.parser().build().parse(keyObject.toString()); + break; + } + } + + if (jwk == null) { + jwksCache.remove(jwksUrl); + continue; // Retry + } + + return jwk.toKey(); + } + + throw new RuntimeException("Failed to fetch JWKS keys for token verification"); + } + } +} diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 79488f234..cdb28ee22 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -22,6 +22,7 @@ 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.featureflag.EE_FEATURES; @@ -42,7 +43,6 @@ 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; import io.supertokens.session.jwt.JWT.JWTException; import io.supertokens.signingkeys.JWTSigningKey; import io.supertokens.signingkeys.SigningKeys; @@ -51,6 +51,7 @@ import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; +import java.io.FileNotFoundException; import java.io.InputStreamReader; import java.lang.reflect.InvocationTargetException; import java.net.HttpURLConnection; @@ -59,9 +60,9 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.util.*; +import java.util.Map.Entry; import java.util.stream.Collectors; public class OAuth { @@ -71,9 +72,6 @@ public class OAuth { private static final String HYDRA_CLIENTS_ENDPOINT = "/admin/clients"; private static final String HYDRA_JWKS_PATH = "/.well-known/jwks.json"; // New constant for JWKS path - private static Map> jwksCache = new HashMap<>(); // Cache for JWKS keys - private static final int MAX_RETRIES = 3; // Maximum number of retries for fetching JWKS - private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException { EE_FEATURES[] features = FeatureFlag.getInstance(main, appIdentifier).getEnabledFeatures(); @@ -88,7 +86,7 @@ private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) "feature."); } - public static Response handleOAuthProxyGET(Main main, AppIdentifier appIdentifier, Storage storage, String path, Map queryParams, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static Response handleOAuthProxyGET(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, Map queryParams, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -103,8 +101,13 @@ public static Response handleOAuthProxyGET(Main main, AppIdentifier appIdentifie queryParams = Transformations.transformQueryParamsForHydra(queryParams); headers = Transformations.transformRequestHeadersForHydra(headers); - String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); - String fullUrl = publicOAuthProviderServiceUrl + path; + String baseURL; + if (proxyToAdmin) { + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); + } else { + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); + } + String fullUrl = baseURL + path; Response response = doGet(fullUrl, headers, queryParams); @@ -117,7 +120,7 @@ public static Response handleOAuthProxyGET(Main main, AppIdentifier appIdentifie return response; } - public static Response handleOAuthProxyFormPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, Map formFields, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static Response handleOAuthProxyFormPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, Map formFields, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -146,6 +149,108 @@ public static Response handleOAuthProxyFormPOST(Main main, AppIdentifier appIden return response; } + public static Response handleOAuthProxyJsonPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + checkForOauthFeature(appIdentifier, main); + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + + if (jsonInput.has("client_id")) { + String clientId = jsonInput.get("client_id").getAsString(); + if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { + 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; + + Response response = doJsonPost(fullUrl, headers, jsonInput); + + // Response transformations + response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); + response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); + + checkNonSuccessResponse(response); + + return response; + } + + public static Response handleOAuthProxyJsonPUT(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + checkForOauthFeature(appIdentifier, main); + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + + if (jsonInput.has("client_id")) { + String clientId = jsonInput.get("client_id").getAsString(); + if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { + 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; + + Response response = doJsonPut(fullUrl, headers, jsonInput); + + // Response transformations + response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); + response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); + + checkNonSuccessResponse(response); + + return response; + } + + public static Response handleOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + checkForOauthFeature(appIdentifier, main); + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + + if (jsonInput.has("client_id")) { + String clientId = jsonInput.get("client_id").getAsString(); + if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { + 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; + + Response response = doJsonDelete(fullUrl, headers, jsonInput); + + // Response transformations + response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); + response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); + + checkNonSuccessResponse(response); + + return response; + } + private static void checkNonSuccessResponse(Response response) throws OAuthAPIException { if (response.statusCode >= 400) { String error = response.jsonResponse.get("error").getAsString(); @@ -183,48 +288,9 @@ private static String reSignToken(AppIdentifier appIdentifier, Main main, String String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); String jwksUrl = publicOAuthProviderServiceUrl + HYDRA_JWKS_PATH; - // Check if cached keys are available for the jwksUrl - Map cachedKeys = jwksCache.get(jwksUrl); - if (cachedKeys == null) { - JsonObject jwksResponse; - try { - jwksResponse = HttpRequest.sendGETRequest(main, "", jwksUrl, null, 10000, 10000, null); - } catch (HttpResponseException e) { - throw new RuntimeException("Could not fetch JWKS keys from hydra for token verification"); - } - cachedKeys = new HashMap<>(); - JsonArray keysArray = jwksResponse.get("keys").getAsJsonArray(); - - // Populate the cache with keys indexed by kid - for (JsonElement key : keysArray) { - JsonObject keyObject = key.getAsJsonObject(); - String kid = keyObject.get("kid").getAsString(); - cachedKeys.put(kid, keyObject); - } - jwksCache.put(jwksUrl, cachedKeys); // Cache the keys with jwksUrl as the key - } - // Validate the JWT and extract claims using the fetched public signing keys - JWT.JWTPreParseInfo jwtInfo = JWT.preParseJWTInfo(token); - JWT.JWTInfo jwtResult = null; - - // Check if the key for the given kid exists in the cache - JsonObject keyObject = cachedKeys.get(jwtInfo.kid); - if (keyObject != null) { - jwtResult = JWT.verifyJWTAndGetPayload(jwtInfo, keyObject.get("n").getAsString(), keyObject.get("e").getAsString()); - } - - if (jwtResult == null) { - // If no matching key found and retry count is not exceeded, refetch the keys - if (retryCount < MAX_RETRIES) { - jwksCache.remove(jwksUrl); // Invalidate cache - return reSignToken(appIdentifier, main, token, iss, stt, useDynamicSigningKey, retryCount + 1); // Retry with incremented count - } else { - throw new RuntimeException("Could not verify token with hydra"); - } - } + JsonObject payload = JWTVerification.verifyJWTAndGetPayload(main, token, jwksUrl); - JsonObject payload = jwtResult.payload; // move keys in ext to root if (payload.has("ext")) { JsonObject ext = payload.getAsJsonObject("ext"); @@ -250,67 +316,14 @@ private static String reSignToken(AppIdentifier appIdentifier, Main main, String return token; } - //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 void addClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException { 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 { - - 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); - } - } + oauthStorage.addClientForApp(appIdentifier, clientId); } - public static JsonObject loadOAuthClient(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) - throws TenantOrAppNotFoundException, InvalidConfigException, StorageQueryException, - IOException, OAuthClientNotFoundException { + public static void removeClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - - String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); - - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException(); - } else { - try { - JsonObject hydraResponse = HttpRequest.sendGETRequest(main, "", adminOAuthProviderServiceUrl + HYDRA_CLIENTS_ENDPOINT + "/" + clientId, null, 10000, 10000, null); - return formatResponseForSDK(hydraResponse); - } catch (HttpResponseException e) { -// throw createCustomExceptionFromHttpResponseException(e, OAuthException.class); - throw new IllegalStateException("FIXME"); // TODO fixme - } - } + oauthStorage.removeAppClientAssociation(appIdentifier, clientId); } public static void deleteOAuthClient(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) @@ -402,25 +415,6 @@ private static T createCustomExceptionFromHttpRespons return customExceptionClass.getDeclaredConstructor(String.class, String.class).newInstance(error, errorDescription); } - private static JsonObject constructHydraRequestParamsForRegisterClientPOST(JsonObject paramsFromSdk, String generatedClientId){ - JsonObject requestBody = new JsonObject(); - - //translating camelCase keys to snakeCase keys - for (Map.Entry jsonEntry : paramsFromSdk.entrySet()){ - requestBody.add(Utils.camelCaseToSnakeCase(jsonEntry.getKey()), jsonEntry.getValue()); - } - - //add client_id - requestBody.addProperty("client_id", generatedClientId); - - //setting other non-changing defaults - requestBody.addProperty("access_token_strategy", "jwt"); - requestBody.addProperty("skip_consent", true); - requestBody.addProperty("subject_type", "public"); - - return requestBody; - } - private static JsonObject formatResponseForSDK(JsonObject response) { JsonObject formattedResponse = new JsonObject(); @@ -441,6 +435,31 @@ public static Map convertCamelToSnakeCase(Map qu return result; } + 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; + } + + public static JsonObject convertSnakeCaseToCamelCaseRecursively(JsonObject jsonResponse) { + if (jsonResponse == null) { + return null; + } + + JsonObject result = new JsonObject(); + for (Entry entry: jsonResponse.entrySet()) { + String key = entry.getKey(); + JsonElement value = entry.getValue(); + if (value.isJsonObject()) { + value = convertSnakeCaseToCamelCaseRecursively(value.getAsJsonObject()); + } + result.add(Utils.snakeCaseToCamelCase(key), value); + } + return result; + } + // HTTP Methods private static Response doGet(String url, Map headers, Map queryParams) throws IOException { @@ -473,41 +492,163 @@ private static Response doGet(String url, Map headers, Map headers, Map formFields) throws IOException { - 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"); + private 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()); + } + } - 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("&"))); + } + int responseCode = con.getResponseCode(); + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + String inputLine; + StringBuffer response = new StringBuffer(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); } + in.close(); + JsonObject jsonResponse = null; + if (con.getContentType().contains("application/json")) { + Gson gson = new Gson(); + jsonResponse = gson.fromJson(response.toString(), JsonObject.class); + } + return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); + } catch (FileNotFoundException e) { + throw new OAuthClientNotFoundException(); } + } - 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("&"))); + private 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/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(jsonInput.toString()); + } + int responseCode = con.getResponseCode(); + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + String inputLine; + StringBuffer response = new StringBuffer(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + JsonObject jsonResponse = null; + if (con.getContentType().contains("application/json")) { + Gson gson = new Gson(); + jsonResponse = gson.fromJson(response.toString(), JsonObject.class); + } + return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); + } catch (FileNotFoundException e) { + throw new OAuthClientNotFoundException(); } - int responseCode = con.getResponseCode(); - BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuffer response = new StringBuffer(); - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); + } + + private static Response doJsonPut(String url, Map headers, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { + try { + URL obj = new URL(url); + 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()); + } + int responseCode = con.getResponseCode(); + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + String inputLine; + StringBuffer response = new StringBuffer(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + JsonObject jsonResponse = null; + if (con.getContentType().contains("application/json")) { + Gson gson = new Gson(); + jsonResponse = gson.fromJson(response.toString(), JsonObject.class); + } + return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); + } catch (FileNotFoundException e) { + throw new OAuthClientNotFoundException(); } - in.close(); - JsonObject jsonResponse = null; - if (con.getContentType().contains("application/json")) { - Gson gson = new Gson(); - jsonResponse = gson.fromJson(response.toString(), JsonObject.class); + } + + private static Response doJsonDelete(String url, Map headers, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { + try { + URL obj = new URL(url); + 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()); + } + } + + int responseCode = con.getResponseCode(); + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + String inputLine; + StringBuffer response = new StringBuffer(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + JsonObject jsonResponse = null; + if (con.getContentType() != null && con.getContentType().contains("application/json")) { + Gson gson = new Gson(); + jsonResponse = gson.fromJson(response.toString(), JsonObject.class); + } + return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); + } catch (FileNotFoundException e) { + throw new OAuthClientNotFoundException(); } - return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); } + public static class Response { public int statusCode; public String rawResponse; diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index 231962c8f..8cc095478 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -121,44 +121,46 @@ public static Map> transformResponseHeadersFromHydra(Main m String redirectTo = headers.get(LOCATION_HEADER_NAME).get(0); - 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), pair.substring(idx + 1)); + if (!redirectTo.startsWith("/")) { + 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), pair.substring(idx + 1)); + } } + String error = urlQueryParams.getOrDefault("error", null); + String errorDebug = urlQueryParams.getOrDefault("error_debug", null); + String errorDescription = urlQueryParams.getOrDefault("error_description", null); + String errorHint = urlQueryParams.getOrDefault("error_hint", null); + throw new OAuthAPIException(error, errorDebug, errorDescription, errorHint, 400); + + } catch (MalformedURLException e) { + throw new IllegalStateException(e); } - String error = urlQueryParams.getOrDefault("error", null); - String errorDebug = urlQueryParams.getOrDefault("error_debug", null); - String errorDescription = urlQueryParams.getOrDefault("error_description", null); - String errorHint = urlQueryParams.getOrDefault("error_hint", null); - throw new OAuthAPIException(error, errorDebug, errorDescription, errorHint, 400); - - } catch (MalformedURLException e) { - throw new IllegalStateException(e); } - } - if (Utils.containsUrl(redirectTo, hydraBaseUrlForConsentAndLogin, true)) { - redirectTo = redirectTo.replace(hydraBaseUrlForConsentAndLogin, "{apiDomain}"); + if (Utils.containsUrl(redirectTo, hydraBaseUrlForConsentAndLogin, true)) { + redirectTo = redirectTo.replace(hydraBaseUrlForConsentAndLogin, "{apiDomain}"); + } + } catch (MalformedURLException e) { + throw new IllegalStateException(e); } - } catch (MalformedURLException e) { - throw new IllegalStateException(e); - } - redirectTo = transformRedirectUrlFromHydra(redirectTo); + redirectTo = transformRedirectUrlFromHydra(redirectTo); + } headers.put(LOCATION_HEADER_NAME, List.of(redirectTo)); } final String COOKIES_HEADER_NAME = "Set-Cookie"; - if(headers.containsKey(COOKIES_HEADER_NAME)) { + if (headers.containsKey(COOKIES_HEADER_NAME)) { // Cookie transformation List cookies = headers.get(COOKIES_HEADER_NAME); cookies = Transformations.transformCookiesFromHydra(cookies); @@ -167,4 +169,24 @@ public static Map> transformResponseHeadersFromHydra(Main m 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; + } } diff --git a/src/main/java/io/supertokens/session/jwt/JWT.java b/src/main/java/io/supertokens/session/jwt/JWT.java index aa75e7a92..05b8288c0 100644 --- a/src/main/java/io/supertokens/session/jwt/JWT.java +++ b/src/main/java/io/supertokens/session/jwt/JWT.java @@ -134,20 +134,6 @@ public static JWTInfo verifyJWTAndGetPayload(JWTPreParseInfo jwt, String publicS return new JWTInfo(new JsonParser().parse(Utils.convertFromBase64(jwt.payload)).getAsJsonObject(), jwt.version); } - public static JWTInfo verifyJWTAndGetPayload(JWTPreParseInfo jwt, String n, String e) - throws InvalidKeyException, NoSuchAlgorithmException, JWTException { - - try { - if (!Utils.verifyWithPublicKey(jwt.header + "." + jwt.payload, jwt.signature, n, e, - jwt.version != AccessToken.VERSION.V1 && jwt.version != AccessToken.VERSION.V2)) { - throw new JWTException("JWT verification failed"); - } - } catch (InvalidKeySpecException | SignatureException e2) { - throw new JWTException("JWT verification failed"); - } - return new JWTInfo(new JsonParser().parse(Utils.convertFromBase64(jwt.payload)).getAsJsonObject(), jwt.version); - } - public static JWTInfo getPayloadWithoutVerifying(String jwt) throws JWTException { JWTPreParseInfo jwtInfo = preParseJWTInfo(jwt); return new JWTInfo(new JsonParser().parse(Utils.convertFromBase64(jwtInfo.payload)).getAsJsonObject(), diff --git a/src/main/java/io/supertokens/utils/Utils.java b/src/main/java/io/supertokens/utils/Utils.java index 5a4936710..9775bb0bb 100644 --- a/src/main/java/io/supertokens/utils/Utils.java +++ b/src/main/java/io/supertokens/utils/Utils.java @@ -316,31 +316,6 @@ public static boolean verifyWithPublicKey(String content, String signature, Stri return sign.verify(decoder.decode(signature)); } - public static boolean verifyWithPublicKey(String content, String signature, String n, String e, boolean urlEncoded) - throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException { - Signature sign = Signature.getInstance("SHA256withRSA"); - byte[] modulusBytes = Base64.getUrlDecoder().decode(n); - byte[] exponentBytes = Base64.getUrlDecoder().decode(e); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - - if ((modulusBytes[0] & 0x80) != 0) { - byte[] newModulusBytes = new byte[modulusBytes.length + 1]; - System.arraycopy(modulusBytes, 0, newModulusBytes, 1, modulusBytes.length); - modulusBytes = newModulusBytes; - } - - BigInteger modulus = new BigInteger(modulusBytes); - BigInteger exponent = new BigInteger(1, exponentBytes); - - RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); - PublicKey pub = keyFactory.generatePublic(spec); - - Base64.Decoder signatureDecoder = urlEncoded ? Base64.getUrlDecoder() : Base64.getDecoder(); - sign.initVerify(pub); - sign.update(content.getBytes()); - return sign.verify(signatureDecoder.decode(signature)); - } - public static boolean isFakeEmail(String email) { return email.endsWith("@stfakeemail.supertokens.com") || email.endsWith(".fakeemail.com"); } diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index 5a19ed282..e48a8c4f9 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -42,6 +42,7 @@ import io.supertokens.webserver.api.oauth.OAuthAuthAPI; import io.supertokens.webserver.api.oauth.CreateUpdateOrGetOAuthClientAPI; import io.supertokens.webserver.api.oauth.OAuthTokenAPI; +import io.supertokens.webserver.api.oauth.RemoveOAuthClientAPI; import io.supertokens.webserver.api.passwordless.*; import io.supertokens.webserver.api.session.*; import io.supertokens.webserver.api.thirdparty.GetUsersByEmailAPI; @@ -273,6 +274,7 @@ private void setupRoutes() { addAPI(new OAuthAuthAPI(main)); addAPI(new OAuthTokenAPI(main)); addAPI(new CreateUpdateOrGetOAuthClientAPI(main)); + addAPI(new RemoveOAuthClientAPI(main)); StandardContext context = tomcatReference.getContext(); Tomcat tomcat = tomcatReference.getTomcat(); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index aa0795238..8719177e4 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -16,164 +16,124 @@ package io.supertokens.webserver.api.oauth; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + 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.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; 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 CreateUpdateOrGetOAuthClientAPI 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"; - +public class CreateUpdateOrGetOAuthClientAPI extends OAuthProxyBase { @Override public String getPath() { return "/recipe/oauth/clients"; } + public CreateUpdateOrGetOAuthClientAPI(Main main){ - super(main, RECIPE_ID.OAUTH.toString()); + super(main); } @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); + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) throws ServletException { + String clientId = ""; - 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); + if (req.getMethod().equals("GET")) { + clientId = InputParser.getQueryParamOrThrowError(req, "clientId", false); + } else if (req.getMethod().equals("PUT")) { + clientId = InputParser.parseStringOrThrowError(input, "clientId", false); } + + return new ProxyProps[] { + new ProxyProps( + "GET", // apiMethod + "GET", // method + "/admin/clients/" + clientId, // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ), + new ProxyProps( + "POST", // apiMethod + "POST_JSON", // method + "/admin/clients", // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ), + new ProxyProps( + "PUT", // apiMethod + "PUT_JSON", // method + "/admin/clients/" + clientId, // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ) + }; } @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 = new JsonObject(); - sendJsonResponse(200, errorResponse, resp); - - } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException - | StorageQueryException e){ - throw new ServletException(e); + protected Map getQueryParamsForProxy(HttpServletRequest req, JsonObject input) throws IOException, ServletException { + 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; } @Override - protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - JsonObject requestBody = InputParser.parseJsonObjectOrThrowError(req); + protected JsonObject getJsonBodyForProxyPOST(HttpServletRequest req, JsonObject input) + throws IOException, ServletException { - String clientId = InputParser.parseStringOrThrowError(requestBody, "clientId", false); + // Defaults that we require + input.addProperty("accessTokenStrategy", "jwt"); + input.addProperty("skipConsent", true); + input.addProperty("subjectType", "public"); - 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 = new JsonObject(); - sendJsonResponse(200, errorResponse, resp); + return input; + } - } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException - | StorageQueryException e){ - throw new ServletException(e); - } + @Override + protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, + Map> headers, String rawBody, JsonObject jsonBody) + throws IOException, ServletException { + this.sendJsonResponse(200, jsonBody, resp); } @Override - protected void doPatch(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - InputParser.throwErrorOnMissingRequiredField(input, REQUIRED_INPUT_FIELDS_FOR_PATCH); + protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + String clientId = jsonBody.get("clientId").getAsString(); 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 = new JsonObject(); - sendJsonResponse(200, errorResponse, resp); - - } catch (TenantOrAppNotFoundException | InvalidConfigException | StorageQueryException | - InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException - | BadPermissionException ex) { - throw new ServletException(ex); + OAuth.addClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } catch (OAuth2ClientAlreadyExistsForAppException e) { + // ignore } + this.sendJsonResponse(200, jsonBody, resp); } - 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; + @Override + protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + this.sendJsonResponse(200, jsonBody, resp); } } 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 bbba9f608..40ff0bcc7 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -43,13 +43,16 @@ public String getPath() { } @Override - public OAuthProxyBase.ProxyProps getProxyProperties() { - return new OAuthProxyBase.ProxyProps( - new String[]{ "POST" }, // apiMethods - "GET", // method - "/oauth2/auth", // path - false // camelToSnakeCaseConversion - ); + public OAuthProxyBase.ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { + return new OAuthProxyBase.ProxyProps[] { + new OAuthProxyBase.ProxyProps( + "POST", // apiMethod + "GET", // method + "/oauth2/auth", // path + false, // proxyToAdmin + false // camelToSnakeCaseConversion + ) + }; } @Override diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java index 2944acaab..07129ce9a 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java @@ -2,7 +2,6 @@ import java.io.IOException; import java.io.Serial; -import java.util.Arrays; import java.util.List; import java.util.Map; @@ -34,22 +33,85 @@ public OAuthProxyBase(Main main) { super(main, RECIPE_ID.OAUTH.toString()); } - public abstract ProxyProps getProxyProperties(); + public abstract ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) throws ServletException; @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - ProxyProps proxyProps = getProxyProperties(); - if (!Arrays.asList(proxyProps.apiMethods).contains(req.getMethod())) { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + ProxyProps[] proxyPropsList = getProxyProperties(req, null); + ProxyProps proxyProps = null; + + for (ProxyProps props : proxyPropsList) { + if (props.apiMethod.equals(req.getMethod())) { + proxyProps = props; + break; + } + } + + if (proxyProps == null) { this.sendTextResponse(405, "Method not supported", resp); return; } + if (proxyProps.method.equals("GET")) { + doProxyGetRequest(req, resp, proxyProps, null); + } else { + this.sendTextResponse(405, "Method not supported", resp); + } + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - if (proxyProps.method == "GET") { + ProxyProps[] proxyPropsList = getProxyProperties(req, input); + ProxyProps proxyProps = null; + + for (ProxyProps props : proxyPropsList) { + if (props.apiMethod.equals(req.getMethod())) { + proxyProps = props; + break; + } + } + + if (proxyProps == null) { + this.sendTextResponse(405, "Method not supported", resp); + return; + } + + if (proxyProps.method.equals("GET")) { doProxyGetRequest(req, resp, proxyProps, input); - } else if (proxyProps.method == "POST_FORM") { + } else if (proxyProps.method.equals("POST_FORM")) { doProxyPostFormRequest(req, resp, proxyProps, input); + } else if (proxyProps.method.equals("POST_JSON")) { + doProxyPostJsonRequest(req, resp, proxyProps, input); + } else if (proxyProps.method.equals("DELETE_JSON")) { + doProxyDeleteJsonRequest(req, resp, proxyProps, input); + } + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + ProxyProps[] proxyPropsList = getProxyProperties(req, input); + ProxyProps proxyProps = null; + + for (ProxyProps props : proxyPropsList) { + if (props.apiMethod.equals(req.getMethod())) { + proxyProps = props; + break; + } + } + + if (proxyProps == null) { + this.sendTextResponse(405, "Method not supported", resp); + return; + } + + if (proxyProps.method.equals("PUT_JSON")) { + doProxyPutJsonRequest(req, resp, proxyProps, input); + } else { + this.sendTextResponse(405, "Method not supported", resp); } } @@ -66,7 +128,11 @@ private void doProxyGetRequest(HttpServletRequest req, HttpServletResponse resp, try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - OAuth.Response response = OAuth.handleOAuthProxyGET(main, appIdentifier, storage, proxyProps.path, queryParams, headers); + OAuth.Response response = OAuth.handleOAuthProxyGET(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, queryParams, headers); + + if (proxyProps.camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } handleResponseFromProxyGET(req, resp, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); @@ -104,7 +170,53 @@ private void doProxyPostFormRequest(HttpServletRequest req, HttpServletResponse try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - OAuth.Response response = OAuth.handleOAuthProxyFormPOST(main, appIdentifier, storage, proxyProps.path, formFields, headers); + OAuth.Response response = OAuth.handleOAuthProxyFormPOST(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, formFields, headers); + + if (proxyProps.camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + + handleResponseFromProxyPOST(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); + + } catch (OAuthClientNotFoundException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); + + this.sendJsonResponse(400, response, resp); + + } catch (OAuthAPIException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "OAUTH_ERROR"); + response.addProperty("error", e.error); + response.addProperty("error_debug", e.errorDebug); + response.addProperty("error_description", e.errorDescription); + response.addProperty("error_hint", e.errorHint); + response.addProperty("status_code", e.statusCode); + this.sendJsonResponse(200, response, resp); + + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { + throw new ServletException(e); + } + } + + private void doProxyPostJsonRequest(HttpServletRequest req, HttpServletResponse resp, ProxyProps proxyProps, JsonObject input) + throws IOException, ServletException { + JsonObject jsonInput = getJsonBodyForProxyPOST(req, input); + + if (proxyProps.camelToSnakeCaseConversion) { + jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); + } + + Map headers = getHeadersForProxy(req, input); + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + OAuth.Response response = OAuth.handleOAuthProxyJsonPOST(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, jsonInput, headers); + + if (proxyProps.camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } handleResponseFromProxyPOST(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); @@ -129,6 +241,90 @@ private void doProxyPostFormRequest(HttpServletRequest req, HttpServletResponse } } + private void doProxyDeleteJsonRequest(HttpServletRequest req, HttpServletResponse resp, ProxyProps proxyProps, JsonObject input) + throws IOException, ServletException { + JsonObject jsonInput = getJsonBodyForProxyDELETE(req, input); + + if (proxyProps.camelToSnakeCaseConversion) { + jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); + } + + Map headers = getHeadersForProxy(req, input); + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + OAuth.Response response = OAuth.handleOAuthProxyJsonDELETE(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, jsonInput, headers); + + if (proxyProps.camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + + handleResponseFromProxyDELETE(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); + + } catch (OAuthClientNotFoundException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); + + this.sendJsonResponse(400, response, resp); + + } catch (OAuthAPIException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "OAUTH_ERROR"); + response.addProperty("error", e.error); + response.addProperty("error_debug", e.errorDebug); + response.addProperty("error_description", e.errorDescription); + response.addProperty("error_hint", e.errorHint); + response.addProperty("status_code", e.statusCode); + this.sendJsonResponse(200, response, resp); + + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { + throw new ServletException(e); + } + } + + private void doProxyPutJsonRequest(HttpServletRequest req, HttpServletResponse resp, ProxyProps proxyProps, JsonObject input) + throws IOException, ServletException { + JsonObject jsonInput = getJsonBodyForProxyPUT(req, input); + + if (proxyProps.camelToSnakeCaseConversion) { + jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); + } + + Map headers = getHeadersForProxy(req, input); + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + OAuth.Response response = OAuth.handleOAuthProxyJsonPUT(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, jsonInput, headers); + + if (proxyProps.camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + + handleResponseFromProxyPUT(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); + + } catch (OAuthClientNotFoundException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); + + this.sendJsonResponse(400, response, resp); + + } catch (OAuthAPIException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "OAUTH_ERROR"); + response.addProperty("error", e.error); + response.addProperty("error_debug", e.errorDebug); + response.addProperty("error_description", e.errorDescription); + response.addProperty("error_hint", e.errorHint); + response.addProperty("status_code", e.statusCode); + this.sendJsonResponse(200, response, resp); + + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { + throw new ServletException(e); + } + } + protected Map getQueryParamsForProxy(HttpServletRequest req, JsonObject input) throws IOException, ServletException { return null; } @@ -141,6 +337,18 @@ protected Map getFormFieldsForProxyPOST(HttpServletRequest req, return null; } + protected JsonObject getJsonBodyForProxyPOST(HttpServletRequest req, JsonObject input) throws IOException, ServletException { + return input; + } + + protected JsonObject getJsonBodyForProxyPUT(HttpServletRequest req, JsonObject input) throws IOException, ServletException { + return input; + } + + protected JsonObject getJsonBodyForProxyDELETE(HttpServletRequest req, JsonObject input) throws IOException, ServletException { + return input; + } + protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { throw new IllegalStateException("Not implemented"); } @@ -149,16 +357,26 @@ protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletRe throw new IllegalStateException("Not implemented"); } + protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + throw new IllegalStateException("Not implemented"); + } + + protected void handleResponseFromProxyDELETE(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + throw new IllegalStateException("Not implemented"); + } + public static class ProxyProps { - public final String[] apiMethods; + public final String apiMethod; public final String method; public final String path; + public final boolean proxyToAdmin; public final boolean camelToSnakeCaseConversion; - public ProxyProps(String[] apiMethods, String method, String path, boolean camelToSnakeCaseConversion) { - this.apiMethods = apiMethods; + public ProxyProps(String apiMethod, String method, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion) { + this.apiMethod = apiMethod; this.method = method; this.path = path; + this.proxyToAdmin = proxyToAdmin; this.camelToSnakeCaseConversion = camelToSnakeCaseConversion; } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 68d6f34a4..bdb6a3101 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -55,13 +55,16 @@ public String getPath() { } @Override - public ProxyProps getProxyProperties() { - return new ProxyProps( - new String[] { "POST" }, // apiMethods - "POST_FORM", // method - "/oauth2/token", // path - false // camelToSnakeCaseConversion - ); + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { + return new ProxyProps[] { + new ProxyProps( + "POST", // apiMethod + "POST_FORM", // method + "/oauth2/token", // path + false, // proxyToAdmin + false // camelToSnakeCaseConversion + ) + }; } @Override 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..bed869605 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java @@ -0,0 +1,81 @@ +/* + * 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.List; +import java.util.Map; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +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 RemoveOAuthClientAPI extends OAuthProxyBase { + @Override + public String getPath() { + return "/recipe/oauth/clients/remove"; + } + + public RemoveOAuthClientAPI(Main main){ + super(main); + } + + @Override + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) throws ServletException { + String clientId = InputParser.parseStringOrThrowError(input, "clientId", false); + + return new ProxyProps[] { + new ProxyProps( + "POST", // apiMethod + "DELETE_JSON", // method + "/admin/clients/" + clientId, // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ) + }; + } + + @Override + protected JsonObject getJsonBodyForProxyDELETE(HttpServletRequest req, JsonObject input) + throws IOException, ServletException { + + return new JsonObject(); + } + + @Override + protected void handleResponseFromProxyDELETE(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + String clientId = InputParser.parseStringOrThrowError(input, "clientId", false); + + try { + OAuth.removeClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("status", "OK"); + this.sendJsonResponse(200, responseBody, resp); + } +} From 8eb8dc216dc8beb5f83ef3dd236e0e9220a1cc1f Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 3 Sep 2024 09:50:20 +0530 Subject: [PATCH 10/40] fix: token type enum --- src/main/java/io/supertokens/oauth/OAuth.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index cdb28ee22..dd792d340 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -264,13 +264,13 @@ private static void checkNonSuccessResponse(Response response) throws OAuthAPIEx public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject jsonBody, String iss, boolean useDynamicKey) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { if (jsonBody.has("access_token")) { String accessToken = jsonBody.get("access_token").getAsString(); - accessToken = reSignToken(appIdentifier, main, accessToken, iss, 1, useDynamicKey, 0); + accessToken = reSignToken(appIdentifier, main, accessToken, iss, SessionTokenType.ACCESS_TOKEN, useDynamicKey, 0); jsonBody.addProperty("access_token", accessToken); } if (jsonBody.has("id_token")) { String idToken = jsonBody.get("id_token").getAsString(); - idToken = reSignToken(appIdentifier, main, idToken, iss, 2, useDynamicKey, 0); + idToken = reSignToken(appIdentifier, main, idToken, iss, SessionTokenType.ID_TOKEN, useDynamicKey, 0); jsonBody.addProperty("id_token", idToken); } @@ -283,7 +283,7 @@ public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, return jsonBody; } - private static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, int stt, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { + private static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, SessionTokenType tokenType, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { // Load the JWKS from the specified URL String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); String jwksUrl = publicOAuthProviderServiceUrl + HYDRA_JWKS_PATH; @@ -300,7 +300,7 @@ private static String reSignToken(AppIdentifier appIdentifier, Main main, String payload.remove("ext"); } payload.addProperty("iss", iss); - payload.addProperty("stt", stt); + payload.addProperty("stt", tokenType.getValue()); JWTSigningKeyInfo keyToUse; if (useDynamicSigningKey) { @@ -662,4 +662,19 @@ public Response(int statusCode, String rawResponse, JsonObject jsonResponse, Map this.headers = headers; } } + + public static enum SessionTokenType { + ACCESS_TOKEN(1), + ID_TOKEN(2); + + private final int value; + + SessionTokenType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } } From b9b2d681ce0b5ba44aeef6113261794ae29b08d2 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 3 Sep 2024 09:57:21 +0530 Subject: [PATCH 11/40] fix: process ext only on access token --- src/main/java/io/supertokens/oauth/OAuth.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index dd792d340..543e99674 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -292,7 +292,7 @@ private static String reSignToken(AppIdentifier appIdentifier, Main main, String JsonObject payload = JWTVerification.verifyJWTAndGetPayload(main, token, jwksUrl); // move keys in ext to root - if (payload.has("ext")) { + if (tokenType == SessionTokenType.ACCESS_TOKEN && payload.has("ext")) { JsonObject ext = payload.getAsJsonObject("ext"); for (Map.Entry entry : ext.entrySet()) { payload.add(entry.getKey(), entry.getValue()); From 04e7c7697d795dc6ca00877f7713602a148e70fc Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 4 Sep 2024 11:31:53 +0530 Subject: [PATCH 12/40] fix: refactor --- .../io/supertokens/oauth/HttpRequest.java | 177 +++++++++ src/main/java/io/supertokens/oauth/OAuth.java | 365 ++---------------- .../webserver/api/oauth/OAuthProxyBase.java | 114 ++---- 3 files changed, 243 insertions(+), 413 deletions(-) create mode 100644 src/main/java/io/supertokens/oauth/HttpRequest.java diff --git a/src/main/java/io/supertokens/oauth/HttpRequest.java b/src/main/java/io/supertokens/oauth/HttpRequest.java new file mode 100644 index 000000000..9472ad117 --- /dev/null +++ b/src/main/java/io/supertokens/oauth/HttpRequest.java @@ -0,0 +1,177 @@ +package io.supertokens.oauth; + +import com.google.gson.Gson; +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.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class HttpRequest { + 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 { + 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 headers, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { + try { + URL obj = new URL(url); + 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, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { + try { + URL obj = new URL(url); + 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(); + BufferedReader in; + if (responseCode < 400) { + in = new BufferedReader(new InputStreamReader(con.getInputStream())); + } else { + in = new BufferedReader(new InputStreamReader(con.getErrorStream())); + } + String inputLine; + StringBuilder response = new StringBuilder(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + JsonObject jsonResponse = null; + if (con.getContentType() != null && con.getContentType().contains("application/json")) { + Gson gson = new Gson(); + jsonResponse = gson.fromJson(response.toString(), JsonObject.class); + } + return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); + } + + public static class Response { + public int statusCode; + public String rawResponse; + public JsonObject jsonResponse; + public Map> headers; + + public Response(int statusCode, String rawResponse, JsonObject 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 543e99674..0e98698e1 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -17,19 +17,14 @@ package io.supertokens.oauth; import com.auth0.jwt.exceptions.JWTCreationException; -import com.google.gson.Gson; -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.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlag; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; -import io.supertokens.httpRequest.HttpRequest; -import io.supertokens.httpRequest.HttpResponseException; import io.supertokens.jwt.JWTSigningFunctions; import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; import io.supertokens.oauth.exceptions.*; @@ -48,29 +43,15 @@ import io.supertokens.signingkeys.SigningKeys; import io.supertokens.utils.Utils; -import java.io.BufferedReader; -import java.io.DataOutputStream; import java.io.IOException; -import java.io.FileNotFoundException; -import java.io.InputStreamReader; -import java.lang.reflect.InvocationTargetException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.*; import java.util.Map.Entry; -import java.util.stream.Collectors; public class OAuth { - private static final int CONNECTION_TIMEOUT = 5000; - private static final int READ_TIMEOUT = 5000; - - private static final String HYDRA_CLIENTS_ENDPOINT = "/admin/clients"; - private static final String HYDRA_JWKS_PATH = "/.well-known/jwks.json"; // New constant for JWKS path + private static final String HYDRA_JWKS_PATH = "/.well-known/jwks.json"; private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException { @@ -86,7 +67,7 @@ private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) "feature."); } - public static Response handleOAuthProxyGET(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, Map queryParams, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequest.Response handleOAuthProxyGET(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, Map queryParams, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -109,7 +90,7 @@ public static Response handleOAuthProxyGET(Main main, AppIdentifier appIdentifie } String fullUrl = baseURL + path; - Response response = doGet(fullUrl, headers, queryParams); + HttpRequest.Response response = HttpRequest.doGet(fullUrl, headers, queryParams); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); @@ -120,7 +101,7 @@ public static Response handleOAuthProxyGET(Main main, AppIdentifier appIdentifie return response; } - public static Response handleOAuthProxyFormPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, Map formFields, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequest.Response handleOAuthProxyFormPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, Map formFields, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -138,7 +119,7 @@ public static Response handleOAuthProxyFormPOST(Main main, AppIdentifier appIden String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); String fullUrl = publicOAuthProviderServiceUrl + path; - Response response = doFormPost(fullUrl, headers, formFields); + HttpRequest.Response response = HttpRequest.doFormPost(fullUrl, headers, formFields); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); @@ -149,7 +130,7 @@ public static Response handleOAuthProxyFormPOST(Main main, AppIdentifier appIden return response; } - public static Response handleOAuthProxyJsonPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequest.Response handleOAuthProxyJsonPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -172,7 +153,7 @@ public static Response handleOAuthProxyJsonPOST(Main main, AppIdentifier appIden } String fullUrl = baseURL + path; - Response response = doJsonPost(fullUrl, headers, jsonInput); + HttpRequest.Response response = HttpRequest.doJsonPost(fullUrl, headers, jsonInput); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); @@ -183,7 +164,7 @@ public static Response handleOAuthProxyJsonPOST(Main main, AppIdentifier appIden return response; } - public static Response handleOAuthProxyJsonPUT(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequest.Response handleOAuthProxyJsonPUT(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -206,18 +187,21 @@ public static Response handleOAuthProxyJsonPUT(Main main, AppIdentifier appIdent } String fullUrl = baseURL + path; - Response response = doJsonPut(fullUrl, headers, jsonInput); + HttpRequest.Response response = HttpRequest.doJsonPut(fullUrl, headers, jsonInput); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); + System.out.println("Status code: " + response.statusCode); + System.out.println("response.jsonResponse: " + response.jsonResponse); + checkNonSuccessResponse(response); return response; } - public static Response handleOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequest.Response handleOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -240,7 +224,7 @@ public static Response handleOAuthProxyJsonDELETE(Main main, AppIdentifier appId } String fullUrl = baseURL + path; - Response response = doJsonDelete(fullUrl, headers, jsonInput); + HttpRequest.Response response = HttpRequest.doJsonDelete(fullUrl, headers, jsonInput); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); @@ -251,12 +235,21 @@ public static Response handleOAuthProxyJsonDELETE(Main main, AppIdentifier appId return response; } - private static void checkNonSuccessResponse(Response response) throws OAuthAPIException { + private static void checkNonSuccessResponse(HttpRequest.Response response) throws OAuthAPIException { if (response.statusCode >= 400) { String error = response.jsonResponse.get("error").getAsString(); - String errorDebug = response.jsonResponse.get("error_debug").getAsString(); - String errorDescription = response.jsonResponse.get("error_description").getAsString(); - String errorHint = response.jsonResponse.get("error_hint").getAsString(); + String errorDebug = null; + if (response.jsonResponse.has("error_debug")) { + errorDebug = response.jsonResponse.get("error_debug").getAsString(); + } + String errorDescription = null; + if (response.jsonResponse.has("error_description")) { + errorDescription = response.jsonResponse.get("error_description").getAsString(); + } + String errorHint = null; + if (response.jsonResponse.has("error_hint")) { + errorHint = response.jsonResponse.get("error_hint").getAsString(); + } throw new OAuthAPIException(error, errorDebug, errorDescription, errorHint, response.statusCode); } } @@ -326,107 +319,6 @@ public static void removeClientId(Main main, AppIdentifier appIdentifier, Storag oauthStorage.removeAppClientAssociation(appIdentifier, clientId); } - public static void deleteOAuthClient(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) - throws TenantOrAppNotFoundException, InvalidConfigException, StorageQueryException, - IOException, OAuthClientNotFoundException { - OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - - String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); - - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException(); - } 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, OAuthException.class); -// } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | -// IllegalAccessException ex) { -// throw new RuntimeException(ex); -// } - throw new IllegalStateException("FIXME"); // TODO fixme - } - } - } - - public static JsonObject updateOauthClient(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject paramsFromSdk) - throws TenantOrAppNotFoundException, InvalidConfigException, StorageQueryException, - InvocationTargetException, NoSuchMethodException, InstantiationException, - IllegalAccessException, OAuthClientNotFoundException, OAuthAPIInvalidInputException, - OAuthClientUpdateException { - - OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); - - String clientId = paramsFromSdk.get("clientId").getAsString(); - - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - throw new OAuthClientNotFoundException(); - } 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 new OAuthClientNotFoundException(); - case 500 -> throw createCustomExceptionFromHttpResponseException(e, OAuthClientUpdateException.class); // hydra is not so helpful with the error messages at this endpoint.. - default -> throw new RuntimeException(e); - } - } - } - } - - 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! - } - hydraPatchFormat.add(translateToHydraPatch(changeIt.getKey(),changeIt.getValue())); - } - - return hydraPatchFormat; - } - - 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); - - 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); - } - - private static JsonObject formatResponseForSDK(JsonObject response) { - JsonObject formattedResponse = new JsonObject(); - - //translating snake_case keys to camelCase keys - for (Map.Entry jsonEntry : response.entrySet()){ - formattedResponse.add(Utils.snakeCaseToCamelCase(jsonEntry.getKey()), jsonEntry.getValue()); - } - - return formattedResponse; - } - - public static Map convertCamelToSnakeCase(Map queryParams) { Map result = new HashMap<>(); for (Map.Entry entry : queryParams.entrySet()) { @@ -460,209 +352,6 @@ public static JsonObject convertSnakeCaseToCamelCaseRecursively(JsonObject jsonR return result; } - // HTTP Methods - - private static Response doGet(String url, Map headers, Map queryParams) throws IOException { - 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()); - } - } - int responseCode = con.getResponseCode(); - BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuffer response = new StringBuffer(); - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - in.close(); - JsonObject jsonResponse = null; - if (con.getContentType() != null && con.getContentType().contains("application/json")) { - Gson gson = new Gson(); - jsonResponse = gson.fromJson(response.toString(), JsonObject.class); - } - return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); - } - - private 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("&"))); - } - int responseCode = con.getResponseCode(); - BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuffer response = new StringBuffer(); - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - in.close(); - JsonObject jsonResponse = null; - if (con.getContentType().contains("application/json")) { - Gson gson = new Gson(); - jsonResponse = gson.fromJson(response.toString(), JsonObject.class); - } - return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); - } catch (FileNotFoundException e) { - throw new OAuthClientNotFoundException(); - } - } - - private 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/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(jsonInput.toString()); - } - int responseCode = con.getResponseCode(); - BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuffer response = new StringBuffer(); - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - in.close(); - JsonObject jsonResponse = null; - if (con.getContentType().contains("application/json")) { - Gson gson = new Gson(); - jsonResponse = gson.fromJson(response.toString(), JsonObject.class); - } - return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); - } catch (FileNotFoundException e) { - throw new OAuthClientNotFoundException(); - } - } - - private static Response doJsonPut(String url, Map headers, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { - try { - URL obj = new URL(url); - 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()); - } - int responseCode = con.getResponseCode(); - BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuffer response = new StringBuffer(); - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - in.close(); - JsonObject jsonResponse = null; - if (con.getContentType().contains("application/json")) { - Gson gson = new Gson(); - jsonResponse = gson.fromJson(response.toString(), JsonObject.class); - } - return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); - } catch (FileNotFoundException e) { - throw new OAuthClientNotFoundException(); - } - } - - private static Response doJsonDelete(String url, Map headers, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { - try { - URL obj = new URL(url); - 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()); - } - } - - int responseCode = con.getResponseCode(); - BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuffer response = new StringBuffer(); - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - in.close(); - JsonObject jsonResponse = null; - if (con.getContentType() != null && con.getContentType().contains("application/json")) { - Gson gson = new Gson(); - jsonResponse = gson.fromJson(response.toString(), JsonObject.class); - } - return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); - } catch (FileNotFoundException e) { - throw new OAuthClientNotFoundException(); - } - } - - public static class Response { - public int statusCode; - public String rawResponse; - public JsonObject jsonResponse; - public Map> headers; - - public Response(int statusCode, String rawResponse, JsonObject jsonResponse, Map> headers) { - this.statusCode = statusCode; - this.rawResponse = rawResponse; - this.jsonResponse = jsonResponse; - this.headers = headers; - } - } - public static enum SessionTokenType { ACCESS_TOKEN(1), ID_TOKEN(2); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java index 07129ce9a..f385b9ee5 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java @@ -10,6 +10,7 @@ import io.supertokens.Main; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequest; import io.supertokens.oauth.OAuth; import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.oauth.exceptions.OAuthClientNotFoundException; @@ -128,7 +129,7 @@ private void doProxyGetRequest(HttpServletRequest req, HttpServletResponse resp, try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - OAuth.Response response = OAuth.handleOAuthProxyGET(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, queryParams, headers); + HttpRequest.Response response = OAuth.handleOAuthProxyGET(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, queryParams, headers); if (proxyProps.camelToSnakeCaseConversion) { response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); @@ -137,21 +138,9 @@ private void doProxyGetRequest(HttpServletRequest req, HttpServletResponse resp, handleResponseFromProxyGET(req, resp, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); } catch (OAuthClientNotFoundException e) { - JsonObject response = new JsonObject(); - response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); - - this.sendJsonResponse(400, response, resp); - + handleOAuthClientNotFoundException(resp); } catch (OAuthAPIException e) { - JsonObject response = new JsonObject(); - response.addProperty("status", "OAUTH_ERROR"); - response.addProperty("error", e.error); - response.addProperty("error_debug", e.errorDebug); - response.addProperty("error_description", e.errorDescription); - response.addProperty("error_hint", e.errorHint); - response.addProperty("status_code", e.statusCode); - this.sendJsonResponse(200, response, resp); - + handleOAuthAPIException(resp, e); } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { throw new ServletException(e); } @@ -170,7 +159,7 @@ private void doProxyPostFormRequest(HttpServletRequest req, HttpServletResponse try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - OAuth.Response response = OAuth.handleOAuthProxyFormPOST(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, formFields, headers); + HttpRequest.Response response = OAuth.handleOAuthProxyFormPOST(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, formFields, headers); if (proxyProps.camelToSnakeCaseConversion) { response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); @@ -179,21 +168,9 @@ private void doProxyPostFormRequest(HttpServletRequest req, HttpServletResponse handleResponseFromProxyPOST(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); } catch (OAuthClientNotFoundException e) { - JsonObject response = new JsonObject(); - response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); - - this.sendJsonResponse(400, response, resp); - + handleOAuthClientNotFoundException(resp); } catch (OAuthAPIException e) { - JsonObject response = new JsonObject(); - response.addProperty("status", "OAUTH_ERROR"); - response.addProperty("error", e.error); - response.addProperty("error_debug", e.errorDebug); - response.addProperty("error_description", e.errorDescription); - response.addProperty("error_hint", e.errorHint); - response.addProperty("status_code", e.statusCode); - this.sendJsonResponse(200, response, resp); - + handleOAuthAPIException(resp, e); } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { throw new ServletException(e); } @@ -212,7 +189,7 @@ private void doProxyPostJsonRequest(HttpServletRequest req, HttpServletResponse try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - OAuth.Response response = OAuth.handleOAuthProxyJsonPOST(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, jsonInput, headers); + HttpRequest.Response response = OAuth.handleOAuthProxyJsonPOST(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, jsonInput, headers); if (proxyProps.camelToSnakeCaseConversion) { response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); @@ -221,21 +198,9 @@ private void doProxyPostJsonRequest(HttpServletRequest req, HttpServletResponse handleResponseFromProxyPOST(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); } catch (OAuthClientNotFoundException e) { - JsonObject response = new JsonObject(); - response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); - - this.sendJsonResponse(400, response, resp); - + handleOAuthClientNotFoundException(resp); } catch (OAuthAPIException e) { - JsonObject response = new JsonObject(); - response.addProperty("status", "OAUTH_ERROR"); - response.addProperty("error", e.error); - response.addProperty("error_debug", e.errorDebug); - response.addProperty("error_description", e.errorDescription); - response.addProperty("error_hint", e.errorHint); - response.addProperty("status_code", e.statusCode); - this.sendJsonResponse(200, response, resp); - + handleOAuthAPIException(resp, e); } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { throw new ServletException(e); } @@ -254,7 +219,7 @@ private void doProxyDeleteJsonRequest(HttpServletRequest req, HttpServletRespons try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - OAuth.Response response = OAuth.handleOAuthProxyJsonDELETE(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, jsonInput, headers); + HttpRequest.Response response = OAuth.handleOAuthProxyJsonDELETE(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, jsonInput, headers); if (proxyProps.camelToSnakeCaseConversion) { response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); @@ -263,21 +228,9 @@ private void doProxyDeleteJsonRequest(HttpServletRequest req, HttpServletRespons handleResponseFromProxyDELETE(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); } catch (OAuthClientNotFoundException e) { - JsonObject response = new JsonObject(); - response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); - - this.sendJsonResponse(400, response, resp); - + handleOAuthClientNotFoundException(resp); } catch (OAuthAPIException e) { - JsonObject response = new JsonObject(); - response.addProperty("status", "OAUTH_ERROR"); - response.addProperty("error", e.error); - response.addProperty("error_debug", e.errorDebug); - response.addProperty("error_description", e.errorDescription); - response.addProperty("error_hint", e.errorHint); - response.addProperty("status_code", e.statusCode); - this.sendJsonResponse(200, response, resp); - + handleOAuthAPIException(resp, e); } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { throw new ServletException(e); } @@ -296,7 +249,7 @@ private void doProxyPutJsonRequest(HttpServletRequest req, HttpServletResponse r try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - OAuth.Response response = OAuth.handleOAuthProxyJsonPUT(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, jsonInput, headers); + HttpRequest.Response response = OAuth.handleOAuthProxyJsonPUT(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, jsonInput, headers); if (proxyProps.camelToSnakeCaseConversion) { response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); @@ -305,26 +258,37 @@ private void doProxyPutJsonRequest(HttpServletRequest req, HttpServletResponse r handleResponseFromProxyPUT(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); } catch (OAuthClientNotFoundException e) { - JsonObject response = new JsonObject(); - response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); - - this.sendJsonResponse(400, response, resp); - + handleOAuthClientNotFoundException(resp); } catch (OAuthAPIException e) { - JsonObject response = new JsonObject(); - response.addProperty("status", "OAUTH_ERROR"); - response.addProperty("error", e.error); - response.addProperty("error_debug", e.errorDebug); - response.addProperty("error_description", e.errorDescription); - response.addProperty("error_hint", e.errorHint); - response.addProperty("status_code", e.statusCode); - this.sendJsonResponse(200, response, resp); - + handleOAuthAPIException(resp, e); } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { throw new ServletException(e); } } + private void handleOAuthClientNotFoundException(HttpServletResponse resp) throws IOException { + JsonObject response = new JsonObject(); + response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); + this.sendJsonResponse(400, response, resp); + } + + private void handleOAuthAPIException(HttpServletResponse resp, OAuthAPIException e) throws IOException { + JsonObject response = new JsonObject(); + response.addProperty("status", "OAUTH_ERROR"); + response.addProperty("error", e.error); + if (e.errorDebug != null) { + response.addProperty("errorDebug", e.errorDebug); + } + if (e.errorDescription != null) { + response.addProperty("errorDescription", e.errorDescription); + } + if (e.errorHint != null) { + response.addProperty("errorHint", e.errorHint); + } + response.addProperty("statusCode", e.statusCode); + this.sendJsonResponse(200, response, resp); + } + protected Map getQueryParamsForProxy(HttpServletRequest req, JsonObject input) throws IOException, ServletException { return null; } From 874ac8ca5ad0fcf5ae0f30895c78d90fa825f2d3 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 5 Sep 2024 14:28:08 +0530 Subject: [PATCH 13/40] fix: bugs and refactor --- .../io/supertokens/oauth/HttpRequest.java | 4 ++ src/main/java/io/supertokens/oauth/OAuth.java | 10 ++--- .../io/supertokens/oauth/Transformations.java | 2 +- .../CreateUpdateOrGetOAuthClientAPI.java | 39 +++++++++++++++++++ .../webserver/api/oauth/OAuthProxyBase.java | 2 +- 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/HttpRequest.java b/src/main/java/io/supertokens/oauth/HttpRequest.java index 9472ad117..e036e100e 100644 --- a/src/main/java/io/supertokens/oauth/HttpRequest.java +++ b/src/main/java/io/supertokens/oauth/HttpRequest.java @@ -9,6 +9,7 @@ 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; @@ -18,6 +19,9 @@ public class HttpRequest { 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("&"))); diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 0e98698e1..5d882ef1e 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -71,7 +71,7 @@ public static HttpRequest.Response handleOAuthProxyGET(Main main, AppIdentifier checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - if (queryParams.containsKey("client_id")) { + if (queryParams != null && queryParams.containsKey("client_id")) { String clientId = queryParams.get("client_id"); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { throw new OAuthClientNotFoundException(); @@ -193,9 +193,6 @@ public static HttpRequest.Response handleOAuthProxyJsonPUT(Main main, AppIdentif response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); - System.out.println("Status code: " + response.statusCode); - System.out.println("response.jsonResponse: " + response.jsonResponse); - checkNonSuccessResponse(response); return response; @@ -235,7 +232,10 @@ public static HttpRequest.Response handleOAuthProxyJsonDELETE(Main main, AppIden return response; } - private static void checkNonSuccessResponse(HttpRequest.Response response) throws OAuthAPIException { + private static void checkNonSuccessResponse(HttpRequest.Response response) throws OAuthAPIException, OAuthClientNotFoundException { + if (response.statusCode == 404) { + throw new OAuthClientNotFoundException(); + } if (response.statusCode >= 400) { String error = response.jsonResponse.get("error").getAsString(); String errorDebug = null; diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index 8cc095478..9d351be29 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -132,7 +132,7 @@ public static Map> transformResponseHeadersFromHydra(Main m String[] pairs = query.split("&"); for (String pair : pairs) { int idx = pair.indexOf("="); - urlQueryParams.put(pair.substring(0, idx), pair.substring(idx + 1)); + urlQueryParams.put(pair.substring(0, idx), URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8)); } } String error = urlQueryParams.getOrDefault("error", null); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index 8719177e4..f273d28e6 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -23,11 +23,17 @@ import java.util.List; import java.util.Map; +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.HttpRequest; import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.exceptions.OAuthAPIException; +import io.supertokens.oauth.exceptions.OAuthClientNotFoundException; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; @@ -111,6 +117,39 @@ protected JsonObject getJsonBodyForProxyPOST(HttpServletRequest req, JsonObject return input; } + @Override + protected JsonObject getJsonBodyForProxyPUT(HttpServletRequest req, JsonObject input) + throws IOException, ServletException { + // fetch existing config and the apply input on top of it + String clientId = input.get("clientId").getAsString(); + + try { + Map queryParams = new HashMap<>(); + queryParams.put("client_id", clientId); + HttpRequest.Response response = OAuth.handleOAuthProxyGET( + main, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/clients/" + clientId, + true, queryParams, null); + + JsonObject existingConfig = response.jsonResponse; + existingConfig = OAuth.convertSnakeCaseToCamelCaseRecursively(existingConfig); + 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 + } + + return input; + } + @Override protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java index f385b9ee5..6e2a8debd 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java @@ -269,7 +269,7 @@ private void doProxyPutJsonRequest(HttpServletRequest req, HttpServletResponse r private void handleOAuthClientNotFoundException(HttpServletResponse resp) throws IOException { JsonObject response = new JsonObject(); response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); - this.sendJsonResponse(400, response, resp); + this.sendJsonResponse(200, response, resp); } private void handleOAuthAPIException(HttpServletResponse resp, OAuthAPIException e) throws IOException { From 2721620a1c610509b4c949659dda76cf7a911c6c Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 5 Sep 2024 16:08:02 +0530 Subject: [PATCH 14/40] fix: oauth clients list api --- .../java/io/supertokens/inmemorydb/Start.java | 9 +++ .../inmemorydb/queries/OAuthQueries.java | 17 +++++ .../io/supertokens/oauth/HttpRequest.java | 9 ++- src/main/java/io/supertokens/oauth/OAuth.java | 54 ++++++++----- .../io/supertokens/oauth/Transformations.java | 3 +- .../io/supertokens/webserver/Webserver.java | 2 + .../CreateUpdateOrGetOAuthClientAPI.java | 12 +-- .../webserver/api/oauth/OAuthAuthAPI.java | 3 +- .../api/oauth/OAuthClientListAPI.java | 75 +++++++++++++++++++ .../webserver/api/oauth/OAuthProxyBase.java | 9 ++- .../webserver/api/oauth/OAuthTokenAPI.java | 7 +- .../api/oauth/RemoveOAuthClientAPI.java | 3 +- 12 files changed, 164 insertions(+), 39 deletions(-) create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index f0f928a6b..9cb99abbc 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -3046,4 +3046,13 @@ 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); + } + } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java index f0441cbe2..806fc9c6d 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; @@ -51,6 +53,21 @@ public static boolean isClientIdForAppId(Start start, String clientId, AppIdenti }, ResultSet::next); } + public static List listClientsForApp(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String QUERY = "SELECT client_id FROM " + Config.getConfig(start).getOAuthClientTable() + + " 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, String clientId, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { String INSERT = "INSERT INTO " + Config.getConfig(start).getOAuthClientTable() diff --git a/src/main/java/io/supertokens/oauth/HttpRequest.java b/src/main/java/io/supertokens/oauth/HttpRequest.java index e036e100e..9fa05dcf8 100644 --- a/src/main/java/io/supertokens/oauth/HttpRequest.java +++ b/src/main/java/io/supertokens/oauth/HttpRequest.java @@ -1,6 +1,7 @@ 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; @@ -157,10 +158,10 @@ private static Response getResponse(HttpURLConnection con) throws IOException { response.append(inputLine); } in.close(); - JsonObject jsonResponse = null; + JsonElement jsonResponse = null; if (con.getContentType() != null && con.getContentType().contains("application/json")) { Gson gson = new Gson(); - jsonResponse = gson.fromJson(response.toString(), JsonObject.class); + jsonResponse = gson.fromJson(response.toString(), JsonElement.class); } return new Response(responseCode, response.toString(), jsonResponse, con.getHeaderFields()); } @@ -168,10 +169,10 @@ private static Response getResponse(HttpURLConnection con) throws IOException { public static class Response { public int statusCode; public String rawResponse; - public JsonObject jsonResponse; + public JsonElement jsonResponse; public Map> headers; - public Response(int statusCode, String rawResponse, JsonObject jsonResponse, Map> headers) { + public Response(int statusCode, String rawResponse, JsonElement jsonResponse, Map> headers) { this.statusCode = statusCode; this.rawResponse = rawResponse; this.jsonResponse = jsonResponse; diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 5d882ef1e..3a527625e 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -17,6 +17,7 @@ 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; @@ -237,18 +238,18 @@ private static void checkNonSuccessResponse(HttpRequest.Response response) throw throw new OAuthClientNotFoundException(); } if (response.statusCode >= 400) { - String error = response.jsonResponse.get("error").getAsString(); + String error = response.jsonResponse.getAsJsonObject().get("error").getAsString(); String errorDebug = null; - if (response.jsonResponse.has("error_debug")) { - errorDebug = response.jsonResponse.get("error_debug").getAsString(); + if (response.jsonResponse.getAsJsonObject().has("error_debug")) { + errorDebug = response.jsonResponse.getAsJsonObject().get("error_debug").getAsString(); } String errorDescription = null; - if (response.jsonResponse.has("error_description")) { - errorDescription = response.jsonResponse.get("error_description").getAsString(); + if (response.jsonResponse.getAsJsonObject().has("error_description")) { + errorDescription = response.jsonResponse.getAsJsonObject().get("error_description").getAsString(); } String errorHint = null; - if (response.jsonResponse.has("error_hint")) { - errorHint = response.jsonResponse.get("error_hint").getAsString(); + if (response.jsonResponse.getAsJsonObject().has("error_hint")) { + errorHint = response.jsonResponse.getAsJsonObject().get("error_hint").getAsString(); } throw new OAuthAPIException(error, errorDebug, errorDescription, errorHint, response.statusCode); } @@ -319,10 +320,17 @@ public static void removeClientId(Main main, AppIdentifier appIdentifier, Storag 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); + } + public static Map convertCamelToSnakeCase(Map queryParams) { Map result = new HashMap<>(); - for (Map.Entry entry : queryParams.entrySet()) { - result.put(Utils.camelCaseToSnakeCase(entry.getKey()), entry.getValue()); + if (queryParams != null) { + for (Map.Entry entry : queryParams.entrySet()) { + result.put(Utils.camelCaseToSnakeCase(entry.getKey()), entry.getValue()); + } } return result; } @@ -335,21 +343,31 @@ public static JsonObject convertCamelToSnakeCase(JsonObject queryParams) { return result; } - public static JsonObject convertSnakeCaseToCamelCaseRecursively(JsonObject jsonResponse) { + public static JsonElement convertSnakeCaseToCamelCaseRecursively(JsonElement jsonResponse) { if (jsonResponse == null) { return null; } - JsonObject result = new JsonObject(); - for (Entry entry: jsonResponse.entrySet()) { - String key = entry.getKey(); - JsonElement value = entry.getValue(); - if (value.isJsonObject()) { - value = convertSnakeCaseToCamelCaseRecursively(value.getAsJsonObject()); + 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)); } - result.add(Utils.snakeCaseToCamelCase(key), value); + return result; } - return result; + return jsonResponse; + } public static enum SessionTokenType { diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index 9d351be29..f6a9e8c41 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; @@ -96,7 +97,7 @@ public static Map transformFormFieldsForHydra(Map entry : existingConfig.entrySet()) { String key = entry.getKey(); if (!input.has(key)) { @@ -152,14 +152,14 @@ protected JsonObject getJsonBodyForProxyPUT(HttpServletRequest req, JsonObject i @Override protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, - Map> headers, String rawBody, JsonObject jsonBody) + Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { this.sendJsonResponse(200, jsonBody, resp); } @Override - protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { - String clientId = jsonBody.get("clientId").getAsString(); + protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { + String clientId = jsonBody.getAsJsonObject().get("clientId").getAsString(); try { OAuth.addClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); @@ -172,7 +172,7 @@ protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletRe } @Override - protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { this.sendJsonResponse(200, jsonBody, resp); } } 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 40ff0bcc7..ba78e910e 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -17,6 +17,7 @@ package io.supertokens.webserver.api.oauth; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; @@ -79,7 +80,7 @@ protected Map getHeadersForProxy(HttpServletRequest req, JsonObj } @Override - protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { if (headers == null || !headers.containsKey("Location")) { throw new IllegalStateException("Invalid response from hydra"); } 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..7a9988e57 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java @@ -0,0 +1,75 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +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.OAuth; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthClientListAPI extends OAuthProxyBase { + + public OAuthClientListAPI(Main main) { + super(main); + } + + @Override + public String getPath() { + return "/recipe/oauth/clients/list"; + } + + @Override + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { + return new ProxyProps[] { + new ProxyProps( + "GET", // apiMethod + "GET", // method + "/admin/clients", // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ) + }; + } + + @Override + protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, + Map> headers, String rawBody, JsonElement jsonBody) + throws IOException, ServletException { + + JsonObject response = new JsonObject(); + response.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 : jsonBody.getAsJsonArray()) { + if (clientIdsSet.contains(clientElem.getAsJsonObject().get("clientId").getAsString())) { + clients.add(clientElem); + } + } + + response.add("clients", clients); + sendJsonResponse(200, response, resp); + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java index 6e2a8debd..fe37fd29c 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; @@ -313,19 +314,19 @@ protected JsonObject getJsonBodyForProxyDELETE(HttpServletRequest req, JsonObjec return input; } - protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { throw new IllegalStateException("Not implemented"); } - protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { throw new IllegalStateException("Not implemented"); } - protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { throw new IllegalStateException("Not implemented"); } - protected void handleResponseFromProxyDELETE(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + protected void handleResponseFromProxyDELETE(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { throw new IllegalStateException("Not implemented"); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index bdb6a3101..5e1c72bf1 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -30,7 +30,6 @@ 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; @@ -82,7 +81,7 @@ protected Map getFormFieldsForProxyPOST(HttpServletRequest req, } @Override - protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { if (jsonBody == null) { throw new IllegalStateException("unexpected response from hydra"); } @@ -96,13 +95,13 @@ protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletRe try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - jsonBody = OAuth.transformTokens(super.main, appIdentifier, storage, jsonBody, iss, useDynamicKey); + jsonBody = OAuth.transformTokens(super.main, appIdentifier, storage, jsonBody.getAsJsonObject(), iss, useDynamicKey); } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { throw new ServletException(e); } - jsonBody.addProperty("status", "OK"); + jsonBody.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, jsonBody, resp); } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java index bed869605..06e7371ed 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; @@ -65,7 +66,7 @@ protected JsonObject getJsonBodyForProxyDELETE(HttpServletRequest req, JsonObjec } @Override - protected void handleResponseFromProxyDELETE(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonObject jsonBody) throws IOException, ServletException { + protected void handleResponseFromProxyDELETE(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { String clientId = InputParser.parseStringOrThrowError(input, "clientId", false); try { From 6f0395921f08f59025d68c3509cc51aa1b6488ee Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 5 Sep 2024 18:37:01 +0530 Subject: [PATCH 15/40] fix: consent get accept and reject --- .../io/supertokens/oauth/HttpRequest.java | 9 +- src/main/java/io/supertokens/oauth/OAuth.java | 14 +-- .../io/supertokens/oauth/Transformations.java | 100 ++++++++++-------- .../io/supertokens/webserver/Webserver.java | 7 ++ .../OAuthAcceptAuthConsentRequestAPI.java | 45 ++++++++ .../oauth/OAuthGetAuthConsentRequestAPI.java | 48 +++++++++ .../webserver/api/oauth/OAuthProxyBase.java | 15 ++- .../OAuthRejectAuthConsentRequestAPI.java | 45 ++++++++ 8 files changed, 229 insertions(+), 54 deletions(-) create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java diff --git a/src/main/java/io/supertokens/oauth/HttpRequest.java b/src/main/java/io/supertokens/oauth/HttpRequest.java index 9fa05dcf8..3524e1090 100644 --- a/src/main/java/io/supertokens/oauth/HttpRequest.java +++ b/src/main/java/io/supertokens/oauth/HttpRequest.java @@ -91,9 +91,14 @@ public static Response doJsonPost(String url, Map headers, JsonO } } - public static Response doJsonPut(String url, Map headers, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { + public static Response doJsonPut(String url, Map queryParams, Map headers, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { try { - URL obj = new URL(url); + if (queryParams == null) { + queryParams = new HashMap<>(); + } + URL obj = new URL(url + "?" + queryParams.entrySet().stream() + .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&"))); HttpURLConnection con = (HttpURLConnection) obj.openConnection(); con.setRequestMethod("PUT"); con.setConnectTimeout(CONNECTION_TIMEOUT); diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 3a527625e..4636bf15b 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -94,7 +94,7 @@ public static HttpRequest.Response handleOAuthProxyGET(Main main, AppIdentifier HttpRequest.Response response = HttpRequest.doGet(fullUrl, headers, queryParams); // Response transformations - response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); + response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); checkNonSuccessResponse(response); @@ -123,7 +123,7 @@ public static HttpRequest.Response handleOAuthProxyFormPOST(Main main, AppIdenti HttpRequest.Response response = HttpRequest.doFormPost(fullUrl, headers, formFields); // Response transformations - response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); + response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); checkNonSuccessResponse(response); @@ -157,7 +157,7 @@ public static HttpRequest.Response handleOAuthProxyJsonPOST(Main main, AppIdenti HttpRequest.Response response = HttpRequest.doJsonPost(fullUrl, headers, jsonInput); // Response transformations - response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); + response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); checkNonSuccessResponse(response); @@ -165,7 +165,7 @@ public static HttpRequest.Response handleOAuthProxyJsonPOST(Main main, AppIdenti return response; } - public static HttpRequest.Response handleOAuthProxyJsonPUT(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequest.Response handleOAuthProxyJsonPUT(Main main, AppIdentifier appIdentifier, Storage storage, String path, Map queryParams, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -188,10 +188,10 @@ public static HttpRequest.Response handleOAuthProxyJsonPUT(Main main, AppIdentif } String fullUrl = baseURL + path; - HttpRequest.Response response = HttpRequest.doJsonPut(fullUrl, headers, jsonInput); + HttpRequest.Response response = HttpRequest.doJsonPut(fullUrl, queryParams, headers, jsonInput); // Response transformations - response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); + response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); checkNonSuccessResponse(response); @@ -225,7 +225,7 @@ public static HttpRequest.Response handleOAuthProxyJsonDELETE(Main main, AppIden HttpRequest.Response response = HttpRequest.doJsonDelete(fullUrl, headers, jsonInput); // Response transformations - response.jsonResponse = Transformations.transformJsonResponseFromHydra(response.jsonResponse); + response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); response.headers = Transformations.transformResponseHeadersFromHydra(main, appIdentifier, response.headers); checkNonSuccessResponse(response); diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index f6a9e8c41..7f126405e 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -97,10 +97,65 @@ public static Map transformFormFieldsForHydra(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 errorDebug = urlQueryParams.getOrDefault("error_debug", null); + String errorDescription = urlQueryParams.getOrDefault("error_description", null); + String errorHint = urlQueryParams.getOrDefault("error_hint", null); + if (error != null) { + throw new OAuthAPIException(error, errorDebug, errorDescription, errorHint, 400); + } + redirectTo = redirectTo.replace(hydraInternalAddress, "{apiDomain}"); + 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 { @@ -114,49 +169,8 @@ public static Map> transformResponseHeadersFromHydra(Main m final String LOCATION_HEADER_NAME = "Location"; if (headers.containsKey(LOCATION_HEADER_NAME)) { - String hydraInternalAddress = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main) - .getOauthProviderUrlConfiguredInHydra(); - String hydraBaseUrlForConsentAndLogin = Config - .getConfig(appIdentifier.getAsPublicTenantIdentifier(), main) - .getOauthProviderConsentLoginBaseUrl(); - String redirectTo = headers.get(LOCATION_HEADER_NAME).get(0); - - if (!redirectTo.startsWith("/")) { - 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 errorDebug = urlQueryParams.getOrDefault("error_debug", null); - String errorDescription = urlQueryParams.getOrDefault("error_description", null); - String errorHint = urlQueryParams.getOrDefault("error_hint", null); - throw new OAuthAPIException(error, errorDebug, errorDescription, errorHint, 400); - - } catch (MalformedURLException e) { - throw new IllegalStateException(e); - } - } - - if (Utils.containsUrl(redirectTo, hydraBaseUrlForConsentAndLogin, true)) { - redirectTo = redirectTo.replace(hydraBaseUrlForConsentAndLogin, "{apiDomain}"); - } - } catch (MalformedURLException e) { - throw new IllegalStateException(e); - } - - redirectTo = transformRedirectUrlFromHydra(redirectTo); - } - + redirectTo = transformRedirectUrlFromHydra(main, appIdentifier, redirectTo); headers.put(LOCATION_HEADER_NAME, List.of(redirectTo)); } diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index 3a2b2829b..588887ac9 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -41,7 +41,10 @@ import io.supertokens.webserver.api.multitenancy.thirdparty.RemoveThirdPartyConfigAPI; import io.supertokens.webserver.api.oauth.OAuthAuthAPI; import io.supertokens.webserver.api.oauth.OAuthClientListAPI; +import io.supertokens.webserver.api.oauth.OAuthGetAuthConsentRequestAPI; +import io.supertokens.webserver.api.oauth.OAuthRejectAuthConsentRequestAPI; import io.supertokens.webserver.api.oauth.CreateUpdateOrGetOAuthClientAPI; +import io.supertokens.webserver.api.oauth.OAuthAcceptAuthConsentRequestAPI; import io.supertokens.webserver.api.oauth.OAuthTokenAPI; import io.supertokens.webserver.api.oauth.RemoveOAuthClientAPI; import io.supertokens.webserver.api.passwordless.*; @@ -278,6 +281,10 @@ private void setupRoutes() { addAPI(new OAuthClientListAPI(main)); addAPI(new RemoveOAuthClientAPI(main)); + addAPI(new OAuthGetAuthConsentRequestAPI(main)); + addAPI(new OAuthAcceptAuthConsentRequestAPI(main)); + addAPI(new OAuthRejectAuthConsentRequestAPI(main)); + StandardContext context = tomcatReference.getContext(); Tomcat tomcat = tomcatReference.getTomcat(); 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..194a2542f --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java @@ -0,0 +1,45 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthAcceptAuthConsentRequestAPI extends OAuthProxyBase { + + public OAuthAcceptAuthConsentRequestAPI(Main main) { + super(main); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/consent/accept"; + } + + @Override + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { + return new ProxyProps[] { + new ProxyProps( + "PUT", // apiMethod + "PUT_JSON", // method + "/admin/oauth2/auth/requests/consent/accept", // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ) + }; + } + + @Override + protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, resp); + } +} 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..624580ad6 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java @@ -0,0 +1,48 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthGetAuthConsentRequestAPI extends OAuthProxyBase { + + public OAuthGetAuthConsentRequestAPI(Main main) { + super(main); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/consent"; + } + + @Override + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { + return new ProxyProps[] { + new ProxyProps( + "GET", // apiMethod + "GET", // method + "/admin/oauth2/auth/requests/consent", // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ) + }; + } + + @Override + protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, + Map> headers, String rawBody, JsonElement jsonBody) + throws IOException, ServletException { + + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, resp); + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java index fe37fd29c..41b8ccddc 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.Serial; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -239,6 +240,12 @@ private void doProxyDeleteJsonRequest(HttpServletRequest req, HttpServletRespons private void doProxyPutJsonRequest(HttpServletRequest req, HttpServletResponse resp, ProxyProps proxyProps, JsonObject input) throws IOException, ServletException { + Map queryParams = getQueryParamsForProxy(req, input); + + if (proxyProps.camelToSnakeCaseConversion) { + queryParams = OAuth.convertCamelToSnakeCase(queryParams); + } + JsonObject jsonInput = getJsonBodyForProxyPUT(req, input); if (proxyProps.camelToSnakeCaseConversion) { @@ -250,7 +257,7 @@ private void doProxyPutJsonRequest(HttpServletRequest req, HttpServletResponse r try { AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - HttpRequest.Response response = OAuth.handleOAuthProxyJsonPUT(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, jsonInput, headers); + HttpRequest.Response response = OAuth.handleOAuthProxyJsonPUT(main, appIdentifier, storage, proxyProps.path, queryParams, proxyProps.proxyToAdmin, jsonInput, headers); if (proxyProps.camelToSnakeCaseConversion) { response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); @@ -291,7 +298,11 @@ private void handleOAuthAPIException(HttpServletResponse resp, OAuthAPIException } protected Map getQueryParamsForProxy(HttpServletRequest req, JsonObject input) throws IOException, ServletException { - return null; + Map queryParams = new HashMap<>(); + for (Map.Entry entry : req.getParameterMap().entrySet()) { + queryParams.put(entry.getKey(), entry.getValue()[0]); + } + return queryParams; } protected Map getHeadersForProxy(HttpServletRequest req, JsonObject input) throws IOException, ServletException { 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..26fa0311b --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java @@ -0,0 +1,45 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthRejectAuthConsentRequestAPI extends OAuthProxyBase { + + public OAuthRejectAuthConsentRequestAPI(Main main) { + super(main); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/consent/reject"; + } + + @Override + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { + return new ProxyProps[] { + new ProxyProps( + "PUT", // apiMethod + "PUT_JSON", // method + "/admin/oauth2/auth/requests/consent/reject", // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ) + }; + } + + @Override + protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, resp); + } +} From 41836b338f64e339a324f51d39ea395179baa4f1 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 6 Sep 2024 13:29:50 +0530 Subject: [PATCH 16/40] fix: login request --- .../io/supertokens/webserver/Webserver.java | 6 +++ .../oauth/OAuthAcceptAuthLoginRequestAPI.java | 45 +++++++++++++++++ .../oauth/OAuthGetAuthLoginRequestAPI.java | 48 +++++++++++++++++++ .../oauth/OAuthRejectAuthLoginRequestAPI.java | 45 +++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index 588887ac9..f27d256ac 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -42,9 +42,12 @@ import io.supertokens.webserver.api.oauth.OAuthAuthAPI; import io.supertokens.webserver.api.oauth.OAuthClientListAPI; import io.supertokens.webserver.api.oauth.OAuthGetAuthConsentRequestAPI; +import io.supertokens.webserver.api.oauth.OAuthGetAuthLoginRequestAPI; import io.supertokens.webserver.api.oauth.OAuthRejectAuthConsentRequestAPI; +import io.supertokens.webserver.api.oauth.OAuthRejectAuthLoginRequestAPI; import io.supertokens.webserver.api.oauth.CreateUpdateOrGetOAuthClientAPI; import io.supertokens.webserver.api.oauth.OAuthAcceptAuthConsentRequestAPI; +import io.supertokens.webserver.api.oauth.OAuthAcceptAuthLoginRequestAPI; import io.supertokens.webserver.api.oauth.OAuthTokenAPI; import io.supertokens.webserver.api.oauth.RemoveOAuthClientAPI; import io.supertokens.webserver.api.passwordless.*; @@ -284,6 +287,9 @@ private void setupRoutes() { addAPI(new OAuthGetAuthConsentRequestAPI(main)); addAPI(new OAuthAcceptAuthConsentRequestAPI(main)); addAPI(new OAuthRejectAuthConsentRequestAPI(main)); + addAPI(new OAuthGetAuthLoginRequestAPI(main)); + addAPI(new OAuthAcceptAuthLoginRequestAPI(main)); + addAPI(new OAuthRejectAuthLoginRequestAPI(main)); StandardContext context = tomcatReference.getContext(); Tomcat tomcat = tomcatReference.getTomcat(); 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..449e09da9 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java @@ -0,0 +1,45 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthAcceptAuthLoginRequestAPI extends OAuthProxyBase { + + public OAuthAcceptAuthLoginRequestAPI(Main main) { + super(main); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/login/accept"; + } + + @Override + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { + return new ProxyProps[] { + new ProxyProps( + "PUT", // apiMethod + "PUT_JSON", // method + "/admin/oauth2/auth/requests/login/accept", // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ) + }; + } + + @Override + protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, resp); + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java new file mode 100644 index 000000000..586e74aad --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java @@ -0,0 +1,48 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthGetAuthLoginRequestAPI extends OAuthProxyBase { + + public OAuthGetAuthLoginRequestAPI(Main main) { + super(main); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/login"; + } + + @Override + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { + return new ProxyProps[] { + new ProxyProps( + "GET", // apiMethod + "GET", // method + "/admin/oauth2/auth/requests/login", // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ) + }; + } + + @Override + protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, + Map> headers, String rawBody, JsonElement jsonBody) + throws IOException, ServletException { + + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, resp); + } +} 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..78c40b8f7 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java @@ -0,0 +1,45 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthRejectAuthLoginRequestAPI extends OAuthProxyBase { + + public OAuthRejectAuthLoginRequestAPI(Main main) { + super(main); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/login/reject"; + } + + @Override + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { + return new ProxyProps[] { + new ProxyProps( + "PUT", // apiMethod + "PUT_JSON", // method + "/admin/oauth2/auth/requests/login/reject", // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ) + }; + } + + @Override + protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, resp); + } +} From 3a10264f822ee24648d00dcca95e26925c298cb0 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 6 Sep 2024 18:42:24 +0530 Subject: [PATCH 17/40] fix: query param transformation --- .../io/supertokens/oauth/Transformations.java | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index 7f126405e..f5979c72d 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -41,7 +41,7 @@ public static Map transformRequestHeadersForHydra(Map 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; @@ -119,6 +131,8 @@ private static String transformRedirectUrlFromHydra(Main main, AppIdentifier app .getOauthProviderConsentLoginBaseUrl(); if (!redirectTo.startsWith("/")) { + redirectTo = transformQueryParamsInURLFromHydra(redirectTo); + try { if (Utils.containsUrl(redirectTo, hydraInternalAddress, true)) { try { From 1eddb69568dc6f5d345a9126966ba77b47bdc807 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 9 Sep 2024 13:00:51 +0530 Subject: [PATCH 18/40] fix: logout request --- .../io/supertokens/webserver/Webserver.java | 6 +++ .../OAuthAcceptAuthLogoutRequestAPI.java | 45 +++++++++++++++++ .../oauth/OAuthGetAuthLogoutRequestAPI.java | 48 +++++++++++++++++++ .../OAuthRejectAuthLogoutRequestAPI.java | 45 +++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index f27d256ac..89ead8fe1 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -43,11 +43,14 @@ import io.supertokens.webserver.api.oauth.OAuthClientListAPI; import io.supertokens.webserver.api.oauth.OAuthGetAuthConsentRequestAPI; import io.supertokens.webserver.api.oauth.OAuthGetAuthLoginRequestAPI; +import io.supertokens.webserver.api.oauth.OAuthGetAuthLogoutRequestAPI; import io.supertokens.webserver.api.oauth.OAuthRejectAuthConsentRequestAPI; import io.supertokens.webserver.api.oauth.OAuthRejectAuthLoginRequestAPI; +import io.supertokens.webserver.api.oauth.OAuthRejectAuthLogoutRequestAPI; import io.supertokens.webserver.api.oauth.CreateUpdateOrGetOAuthClientAPI; import io.supertokens.webserver.api.oauth.OAuthAcceptAuthConsentRequestAPI; import io.supertokens.webserver.api.oauth.OAuthAcceptAuthLoginRequestAPI; +import io.supertokens.webserver.api.oauth.OAuthAcceptAuthLogoutRequestAPI; import io.supertokens.webserver.api.oauth.OAuthTokenAPI; import io.supertokens.webserver.api.oauth.RemoveOAuthClientAPI; import io.supertokens.webserver.api.passwordless.*; @@ -290,6 +293,9 @@ 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)); StandardContext context = tomcatReference.getContext(); Tomcat tomcat = tomcatReference.getTomcat(); 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..1533cb4d0 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java @@ -0,0 +1,45 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthAcceptAuthLogoutRequestAPI extends OAuthProxyBase { + + public OAuthAcceptAuthLogoutRequestAPI(Main main) { + super(main); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/logout/accept"; + } + + @Override + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { + return new ProxyProps[] { + new ProxyProps( + "PUT", // apiMethod + "PUT_JSON", // method + "/admin/oauth2/auth/requests/logout/accept", // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ) + }; + } + + @Override + protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, resp); + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java new file mode 100644 index 000000000..90195c61f --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java @@ -0,0 +1,48 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthGetAuthLogoutRequestAPI extends OAuthProxyBase { + + public OAuthGetAuthLogoutRequestAPI(Main main) { + super(main); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/logout"; + } + + @Override + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { + return new ProxyProps[] { + new ProxyProps( + "GET", // apiMethod + "GET", // method + "/admin/oauth2/auth/requests/logout", // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ) + }; + } + + @Override + protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, + Map> headers, String rawBody, JsonElement jsonBody) + throws IOException, ServletException { + + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, resp); + } +} 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..3f89171a5 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java @@ -0,0 +1,45 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthRejectAuthLogoutRequestAPI extends OAuthProxyBase { + + public OAuthRejectAuthLogoutRequestAPI(Main main) { + super(main); + } + + @Override + public String getPath() { + return "/recipe/oauth/auth/requests/logout/reject"; + } + + @Override + public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { + return new ProxyProps[] { + new ProxyProps( + "PUT", // apiMethod + "PUT_JSON", // method + "/admin/oauth2/auth/requests/logout/reject", // path + true, // proxyToAdmin + true // camelToSnakeCaseConversion + ) + }; + } + + @Override + protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, resp); + } +} From 84fda3f7c143f8dd77013b9e877adb242450b9c4 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 9 Sep 2024 18:19:05 +0530 Subject: [PATCH 19/40] fix: refactor --- src/main/java/io/supertokens/oauth/OAuth.java | 9 +- .../CreateUpdateOrGetOAuthClientAPI.java | 211 +++++----- .../OAuthAcceptAuthConsentRequestAPI.java | 52 ++- .../oauth/OAuthAcceptAuthLoginRequestAPI.java | 54 ++- .../OAuthAcceptAuthLogoutRequestAPI.java | 54 ++- .../webserver/api/oauth/OAuthAuthAPI.java | 113 +++--- .../api/oauth/OAuthClientListAPI.java | 76 ++-- .../oauth/OAuthGetAuthConsentRequestAPI.java | 52 +-- .../oauth/OAuthGetAuthLoginRequestAPI.java | 52 +-- .../oauth/OAuthGetAuthLogoutRequestAPI.java | 52 +-- .../webserver/api/oauth/OAuthProxyBase.java | 359 ------------------ .../webserver/api/oauth/OAuthProxyHelper.java | 284 ++++++++++++++ .../OAuthRejectAuthConsentRequestAPI.java | 52 ++- .../oauth/OAuthRejectAuthLoginRequestAPI.java | 52 ++- .../OAuthRejectAuthLogoutRequestAPI.java | 51 ++- .../webserver/api/oauth/OAuthTokenAPI.java | 90 ++--- .../api/oauth/RemoveOAuthClientAPI.java | 68 ++-- 17 files changed, 856 insertions(+), 825 deletions(-) delete mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 4636bf15b..11ec6fa85 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -117,8 +117,13 @@ public static HttpRequest.Response handleOAuthProxyFormPOST(Main main, AppIdenti formFields = Transformations.transformFormFieldsForHydra(formFields); headers = Transformations.transformRequestHeadersForHydra(headers); - String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); - String fullUrl = publicOAuthProviderServiceUrl + path; + String baseURL; + if (proxyToAdmin) { + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); + } else { + baseURL = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); + } + String fullUrl = baseURL + path; HttpRequest.Response response = HttpRequest.doFormPost(fullUrl, headers, formFields); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index 1dc3ea794..174a3e927 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -20,7 +20,6 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.List; import java.util.Map; import com.google.gson.JsonElement; @@ -33,146 +32,160 @@ import io.supertokens.oauth.OAuth; import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.oauth.exceptions.OAuthClientNotFoundException; +import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -public class CreateUpdateOrGetOAuthClientAPI extends OAuthProxyBase { +public class CreateUpdateOrGetOAuthClientAPI extends WebserverAPI { @Override public String getPath() { return "/recipe/oauth/clients"; } public CreateUpdateOrGetOAuthClientAPI(Main main){ - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) throws ServletException { - String clientId = ""; - - if (req.getMethod().equals("GET")) { - clientId = InputParser.getQueryParamOrThrowError(req, "clientId", false); - } else if (req.getMethod().equals("PUT")) { - clientId = InputParser.parseStringOrThrowError(input, "clientId", false); - } - - return new ProxyProps[] { - new ProxyProps( - "GET", // apiMethod - "GET", // method - "/admin/clients/" + clientId, // path - true, // proxyToAdmin - true // camelToSnakeCaseConversion - ), - new ProxyProps( - "POST", // apiMethod - "POST_JSON", // method - "/admin/clients", // path - true, // proxyToAdmin - true // camelToSnakeCaseConversion - ), - new ProxyProps( - "PUT", // apiMethod - "PUT_JSON", // method - "/admin/clients/" + clientId, // path + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + String clientId = InputParser.getQueryParamOrThrowError(req, "clientId", false); + + try { + OAuthProxyHelper.proxyGET( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/clients/" + clientId, // proxyPath true, // proxyToAdmin - true // camelToSnakeCaseConversion - ) - }; - } - - @Override - protected Map getQueryParamsForProxy(HttpServletRequest req, JsonObject input) throws IOException, ServletException { - 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)); + true, // camelToSnakeCaseConversion + () -> { // getQueryParamsForProxy + 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; + }, + () -> { // getHeadersForProxy + return new HashMap<>(); + }, + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + this.sendJsonResponse(200, jsonBody, resp); } - } + ); + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); } - - return queryParams; } @Override - protected JsonObject getJsonBodyForProxyPOST(HttpServletRequest req, JsonObject input) - throws IOException, ServletException { + 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"); - return input; - } - - @Override - protected JsonObject getJsonBodyForProxyPUT(HttpServletRequest req, JsonObject input) - throws IOException, ServletException { - // fetch existing config and the apply input on top of it - String clientId = input.get("clientId").getAsString(); - try { - Map queryParams = new HashMap<>(); - queryParams.put("client_id", clientId); - HttpRequest.Response response = OAuth.handleOAuthProxyGET( - main, + OAuthProxyHelper.proxyJsonPOST( + main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), - "/admin/clients/" + clientId, - true, queryParams, null); - - JsonObject existingConfig = response.jsonResponse.getAsJsonObject(); - existingConfig = OAuth.convertSnakeCaseToCamelCaseRecursively(existingConfig).getAsJsonObject(); - for (Map.Entry entry : existingConfig.entrySet()) { - String key = entry.getKey(); - if (!input.has(key)) { - input.add(key, entry.getValue()); + "/admin/clients", // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + () -> { // getJsonBody + return input; + }, + () -> { // getHeadersForProxy + return new HashMap<>(); + }, + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + String clientId = jsonBody.getAsJsonObject().get("clientId").getAsString(); + + try { + OAuth.addClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } catch (OAuth2ClientAlreadyExistsForAppException e) { + // ignore + } + this.sendJsonResponse(200, jsonBody, resp); } - } - } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { + ); + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); - } catch (OAuthClientNotFoundException | OAuthAPIException e) { - // ignore since the PUT API will throw one of this error later on } - - return input; - } - - @Override - protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, - Map> headers, String rawBody, JsonElement jsonBody) - throws IOException, ServletException { - this.sendJsonResponse(200, jsonBody, resp); } @Override - protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - String clientId = jsonBody.getAsJsonObject().get("clientId").getAsString(); + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String clientId = InputParser.parseStringOrThrowError(input, "clientId", false); try { - OAuth.addClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); - } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/clients/" + clientId, + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + () -> { // getJsonBody + return new HashMap<>(); + }, + () -> { // getHeadersForProxy + try { + Map queryParams = new HashMap<>(); + queryParams.put("client_id", clientId); + HttpRequest.Response response = OAuth.handleOAuthProxyGET( + main, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/clients/" + clientId, + true, queryParams, null); + + JsonObject existingConfig = response.jsonResponse.getAsJsonObject(); + existingConfig = OAuth.convertSnakeCaseToCamelCaseRecursively(existingConfig).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 + } + + return input; + }, + () -> { // getHeadersForProxy + return new HashMap<>(); + }, + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + this.sendJsonResponse(200, jsonBody, resp); + } + ); + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); - } catch (OAuth2ClientAlreadyExistsForAppException e) { - // ignore } - this.sendJsonResponse(200, jsonBody, resp); - } - - @Override - protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - this.sendJsonResponse(200, jsonBody, resp); } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java index 194a2542f..17cdb45c1 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java @@ -1,21 +1,27 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.util.List; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +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 OAuthProxyBase { +public class OAuthAcceptAuthConsentRequestAPI extends WebserverAPI { public OAuthAcceptAuthConsentRequestAPI(Main main) { - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override @@ -24,22 +30,28 @@ public String getPath() { } @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { - return new ProxyProps[] { - new ProxyProps( - "PUT", // apiMethod - "PUT_JSON", // method - "/admin/oauth2/auth/requests/consent/accept", // path + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + try { + OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/oauth2/auth/requests/consent/accept", // proxyPath true, // proxyToAdmin - true // camelToSnakeCaseConversion - ) - }; - } - - @Override - protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + true, // camelToSnakeCaseConversion + () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + () -> input, // getJsonBody + HashMap::new, // getHeadersForProxy + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, 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 index 449e09da9..ac76dd2f0 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java @@ -1,21 +1,27 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.util.List; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; -import com.google.gson.JsonElement; 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.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 OAuthProxyBase { +public class OAuthAcceptAuthLoginRequestAPI extends WebserverAPI { public OAuthAcceptAuthLoginRequestAPI(Main main) { - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override @@ -24,22 +30,28 @@ public String getPath() { } @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { - return new ProxyProps[] { - new ProxyProps( - "PUT", // apiMethod - "PUT_JSON", // method - "/admin/oauth2/auth/requests/login/accept", // path - true, // proxyToAdmin - true // camelToSnakeCaseConversion - ) - }; - } - - @Override - protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + try { + OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/oauth2/auth/requests/login/accept", + true, + true, + () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + () -> input, + HashMap::new, + (statusCode, headers, rawBody, jsonBody) -> { + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, 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 index 1533cb4d0..ec6586d2d 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java @@ -1,21 +1,27 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.util.List; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +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 OAuthProxyBase { +public class OAuthAcceptAuthLogoutRequestAPI extends WebserverAPI { public OAuthAcceptAuthLogoutRequestAPI(Main main) { - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override @@ -24,22 +30,28 @@ public String getPath() { } @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { - return new ProxyProps[] { - new ProxyProps( - "PUT", // apiMethod - "PUT_JSON", // method - "/admin/oauth2/auth/requests/logout/accept", // path - true, // proxyToAdmin - true // camelToSnakeCaseConversion - ) - }; - } - - @Override - protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + try { + OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/oauth2/auth/requests/logout/accept", + true, + true, + () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + () -> input, + HashMap::new, + (statusCode, headers, rawBody, jsonBody) -> { + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, 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 ba78e910e..1e103700f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -17,12 +17,15 @@ package io.supertokens.webserver.api.oauth; import com.google.gson.JsonArray; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +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; @@ -33,9 +36,9 @@ import java.util.Map; import java.util.stream.Collectors; -public class OAuthAuthAPI extends OAuthProxyBase { +public class OAuthAuthAPI extends WebserverAPI { public OAuthAuthAPI(Main main) { - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override @@ -44,62 +47,60 @@ public String getPath() { } @Override - public OAuthProxyBase.ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { - return new OAuthProxyBase.ProxyProps[] { - new OAuthProxyBase.ProxyProps( - "POST", // apiMethod - "GET", // method - "/oauth2/auth", // path - false, // proxyToAdmin - false // camelToSnakeCaseConversion - ) - }; - } - - @Override - protected Map getQueryParamsForProxy(HttpServletRequest req, JsonObject input) throws IOException, ServletException { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); JsonObject params = InputParser.parseJsonObjectOrThrowError(input, "params", false); - - return params.entrySet().stream().collect(Collectors.toMap( - Map.Entry::getKey, - e -> e.getValue().getAsString() - )); - } - - @Override - protected Map getHeadersForProxy(HttpServletRequest req, JsonObject input) throws ServletException, IOException { String cookies = InputParser.parseStringOrThrowError(input, "cookies", true); - Map headers = new HashMap<>(); - - if (cookies != null) { - headers.put("Cookie", cookies); - } - - return headers; - } - - @Override - protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - if (headers == null || !headers.containsKey("Location")) { - throw new IllegalStateException("Invalid response from hydra"); + try { + OAuthProxyHelper.proxyGET( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/oauth2/auth", + false, + false, + () -> { + return params.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue().getAsString() + )); + }, + () -> { + Map headers = new HashMap<>(); + + if (cookies != null) { + headers.put("Cookie", cookies); + } + + return headers; + }, + (statusCode, headers, rawBody, jsonBody) -> { + if (headers == null || !headers.containsKey("Location")) { + throw new IllegalStateException("Invalid response from hydra"); + } + + String redirectTo = headers.get("Location").get(0); + List responseCookies = headers.get("Set-Cookie"); + + JsonObject response = new JsonObject(); + response.addProperty("redirectTo", redirectTo); + + JsonArray jsonCookies = new JsonArray(); + if (responseCookies != null) { + for (String cookie : responseCookies) { + jsonCookies.add(new JsonPrimitive(cookie)); + } + } + + response.add("cookies", jsonCookies); + response.addProperty("status", "OK"); + super.sendJsonResponse(200, response, resp); + } + ); + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); } - - String redirectTo = headers.get("Location").get(0); - List cookies = headers.get("Set-Cookie"); - - JsonObject response = new JsonObject(); - response.addProperty("redirectTo", redirectTo); - - JsonArray jsonCookies = new JsonArray(); - if (cookies != null) { - for (String cookie : cookies) { - jsonCookies.add(new JsonPrimitive(cookie)); - } - } - - response.add("cookies", jsonCookies); - response.addProperty("status", "OK"); - super.sendJsonResponse(200, response, resp); } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java index 7a9988e57..9fbdd8085 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java @@ -1,9 +1,9 @@ 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; @@ -13,16 +13,18 @@ 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.exceptions.StorageQueryException; 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 OAuthProxyBase { +public class OAuthClientListAPI extends WebserverAPI { public OAuthClientListAPI(Main main) { - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override @@ -31,45 +33,45 @@ public String getPath() { } @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { - return new ProxyProps[] { - new ProxyProps( - "GET", // apiMethod - "GET", // method - "/admin/clients", // path + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + try { + OAuthProxyHelper.proxyGET( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/clients", // proxyPath true, // proxyToAdmin - true // camelToSnakeCaseConversion - ) - }; - } + true, // camelToSnakeCaseConversion + HashMap::new, // getQueryParamsForProxy + HashMap::new, // getHeadersForProxy + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); - @Override - protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, - Map> headers, String rawBody, JsonElement jsonBody) - throws IOException, ServletException { + // 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); + } - JsonObject response = new JsonObject(); - response.addProperty("status", "OK"); + Set clientIdsSet = new HashSet<>(clientIds); - // 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); - } + JsonArray clients = new JsonArray(); + + for (JsonElement clientElem : jsonBody.getAsJsonArray()) { + if (clientIdsSet.contains(clientElem.getAsJsonObject().get("clientId").getAsString())) { + clients.add(clientElem); + } + } - Set clientIdsSet = new HashSet<>(clientIds); - - JsonArray clients = new JsonArray(); - - for (JsonElement clientElem : jsonBody.getAsJsonArray()) { - if (clientIdsSet.contains(clientElem.getAsJsonObject().get("clientId").getAsString())) { - clients.add(clientElem); - } + response.add("clients", clients); + sendJsonResponse(200, response, resp); + } + ); + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); } - - response.add("clients", clients); - sendJsonResponse(200, response, resp); } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java index 624580ad6..371a0f0b1 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java @@ -1,21 +1,26 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.util.List; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +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 OAuthProxyBase { +public class OAuthGetAuthConsentRequestAPI extends WebserverAPI { public OAuthGetAuthConsentRequestAPI(Main main) { - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override @@ -24,25 +29,26 @@ public String getPath() { } @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { - return new ProxyProps[] { - new ProxyProps( - "GET", // apiMethod - "GET", // method - "/admin/oauth2/auth/requests/consent", // path + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + try { + OAuthProxyHelper.proxyGET( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/oauth2/auth/requests/consent", // proxyPath true, // proxyToAdmin - true // camelToSnakeCaseConversion - ) - }; - } - - @Override - protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, - Map> headers, String rawBody, JsonElement jsonBody) - throws IOException, ServletException { - - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + true, // camelToSnakeCaseConversion + () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + HashMap::new, // getHeadersForProxy + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, 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 index 586e74aad..0c848d7ca 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java @@ -1,21 +1,26 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.util.List; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +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 OAuthProxyBase { +public class OAuthGetAuthLoginRequestAPI extends WebserverAPI { public OAuthGetAuthLoginRequestAPI(Main main) { - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override @@ -24,25 +29,26 @@ public String getPath() { } @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { - return new ProxyProps[] { - new ProxyProps( - "GET", // apiMethod - "GET", // method - "/admin/oauth2/auth/requests/login", // path + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + try { + OAuthProxyHelper.proxyGET( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/oauth2/auth/requests/login", // proxyPath true, // proxyToAdmin - true // camelToSnakeCaseConversion - ) - }; - } - - @Override - protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, - Map> headers, String rawBody, JsonElement jsonBody) - throws IOException, ServletException { - - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + true, // camelToSnakeCaseConversion + () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + HashMap::new, // getHeadersForProxy + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, 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 index 90195c61f..3afa294d7 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java @@ -1,21 +1,26 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.util.List; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +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 OAuthProxyBase { +public class OAuthGetAuthLogoutRequestAPI extends WebserverAPI { public OAuthGetAuthLogoutRequestAPI(Main main) { - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override @@ -24,25 +29,26 @@ public String getPath() { } @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { - return new ProxyProps[] { - new ProxyProps( - "GET", // apiMethod - "GET", // method - "/admin/oauth2/auth/requests/logout", // path + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + try { + OAuthProxyHelper.proxyGET( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/oauth2/auth/requests/logout", // proxyPath true, // proxyToAdmin - true // camelToSnakeCaseConversion - ) - }; - } - - @Override - protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, - Map> headers, String rawBody, JsonElement jsonBody) - throws IOException, ServletException { - - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + true, // camelToSnakeCaseConversion + () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + HashMap::new, // getHeadersForProxy + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, resp); + } + ); + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java deleted file mode 100644 index 41b8ccddc..000000000 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyBase.java +++ /dev/null @@ -1,359 +0,0 @@ -package io.supertokens.webserver.api.oauth; - -import java.io.IOException; -import java.io.Serial; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -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.HttpRequest; -import io.supertokens.oauth.OAuth; -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 abstract class OAuthProxyBase extends WebserverAPI { - @Serial - private static final long serialVersionUID = -8734479943734920904L; - - public OAuthProxyBase(Main main) { - super(main, RECIPE_ID.OAUTH.toString()); - } - - public abstract ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) throws ServletException; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - ProxyProps[] proxyPropsList = getProxyProperties(req, null); - ProxyProps proxyProps = null; - - for (ProxyProps props : proxyPropsList) { - if (props.apiMethod.equals(req.getMethod())) { - proxyProps = props; - break; - } - } - - if (proxyProps == null) { - this.sendTextResponse(405, "Method not supported", resp); - return; - } - - if (proxyProps.method.equals("GET")) { - doProxyGetRequest(req, resp, proxyProps, null); - } else { - this.sendTextResponse(405, "Method not supported", resp); - } - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - - ProxyProps[] proxyPropsList = getProxyProperties(req, input); - ProxyProps proxyProps = null; - - for (ProxyProps props : proxyPropsList) { - if (props.apiMethod.equals(req.getMethod())) { - proxyProps = props; - break; - } - } - - if (proxyProps == null) { - this.sendTextResponse(405, "Method not supported", resp); - return; - } - - if (proxyProps.method.equals("GET")) { - doProxyGetRequest(req, resp, proxyProps, input); - } else if (proxyProps.method.equals("POST_FORM")) { - doProxyPostFormRequest(req, resp, proxyProps, input); - } else if (proxyProps.method.equals("POST_JSON")) { - doProxyPostJsonRequest(req, resp, proxyProps, input); - } else if (proxyProps.method.equals("DELETE_JSON")) { - doProxyDeleteJsonRequest(req, resp, proxyProps, input); - } - } - - @Override - protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - - ProxyProps[] proxyPropsList = getProxyProperties(req, input); - ProxyProps proxyProps = null; - - for (ProxyProps props : proxyPropsList) { - if (props.apiMethod.equals(req.getMethod())) { - proxyProps = props; - break; - } - } - - if (proxyProps == null) { - this.sendTextResponse(405, "Method not supported", resp); - return; - } - - if (proxyProps.method.equals("PUT_JSON")) { - doProxyPutJsonRequest(req, resp, proxyProps, input); - } else { - this.sendTextResponse(405, "Method not supported", resp); - } - } - - private void doProxyGetRequest(HttpServletRequest req, HttpServletResponse resp, ProxyProps proxyProps, JsonObject input) - throws IOException, ServletException { - Map queryParams = getQueryParamsForProxy(req, input); - - if (proxyProps.camelToSnakeCaseConversion) { - queryParams = OAuth.convertCamelToSnakeCase(queryParams); - } - - Map headers = getHeadersForProxy(req, input); - - try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - HttpRequest.Response response = OAuth.handleOAuthProxyGET(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, queryParams, headers); - - if (proxyProps.camelToSnakeCaseConversion) { - response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); - } - - handleResponseFromProxyGET(req, resp, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); - - } catch (OAuthClientNotFoundException e) { - handleOAuthClientNotFoundException(resp); - } catch (OAuthAPIException e) { - handleOAuthAPIException(resp, e); - } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { - throw new ServletException(e); - } - } - - private void doProxyPostFormRequest(HttpServletRequest req, HttpServletResponse resp, ProxyProps proxyProps, JsonObject input) - throws IOException, ServletException { - Map formFields = getFormFieldsForProxyPOST(req, input); - - if (proxyProps.camelToSnakeCaseConversion) { - formFields = OAuth.convertCamelToSnakeCase(formFields); - } - - Map headers = getHeadersForProxy(req, input); - - try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - HttpRequest.Response response = OAuth.handleOAuthProxyFormPOST(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, formFields, headers); - - if (proxyProps.camelToSnakeCaseConversion) { - response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); - } - - handleResponseFromProxyPOST(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); - - } catch (OAuthClientNotFoundException e) { - handleOAuthClientNotFoundException(resp); - } catch (OAuthAPIException e) { - handleOAuthAPIException(resp, e); - } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { - throw new ServletException(e); - } - } - - private void doProxyPostJsonRequest(HttpServletRequest req, HttpServletResponse resp, ProxyProps proxyProps, JsonObject input) - throws IOException, ServletException { - JsonObject jsonInput = getJsonBodyForProxyPOST(req, input); - - if (proxyProps.camelToSnakeCaseConversion) { - jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); - } - - Map headers = getHeadersForProxy(req, input); - - try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - HttpRequest.Response response = OAuth.handleOAuthProxyJsonPOST(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, jsonInput, headers); - - if (proxyProps.camelToSnakeCaseConversion) { - response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); - } - - handleResponseFromProxyPOST(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); - - } catch (OAuthClientNotFoundException e) { - handleOAuthClientNotFoundException(resp); - } catch (OAuthAPIException e) { - handleOAuthAPIException(resp, e); - } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { - throw new ServletException(e); - } - } - - private void doProxyDeleteJsonRequest(HttpServletRequest req, HttpServletResponse resp, ProxyProps proxyProps, JsonObject input) - throws IOException, ServletException { - JsonObject jsonInput = getJsonBodyForProxyDELETE(req, input); - - if (proxyProps.camelToSnakeCaseConversion) { - jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); - } - - Map headers = getHeadersForProxy(req, input); - - try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - HttpRequest.Response response = OAuth.handleOAuthProxyJsonDELETE(main, appIdentifier, storage, proxyProps.path, proxyProps.proxyToAdmin, jsonInput, headers); - - if (proxyProps.camelToSnakeCaseConversion) { - response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); - } - - handleResponseFromProxyDELETE(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); - - } catch (OAuthClientNotFoundException e) { - handleOAuthClientNotFoundException(resp); - } catch (OAuthAPIException e) { - handleOAuthAPIException(resp, e); - } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { - throw new ServletException(e); - } - } - - private void doProxyPutJsonRequest(HttpServletRequest req, HttpServletResponse resp, ProxyProps proxyProps, JsonObject input) - throws IOException, ServletException { - Map queryParams = getQueryParamsForProxy(req, input); - - if (proxyProps.camelToSnakeCaseConversion) { - queryParams = OAuth.convertCamelToSnakeCase(queryParams); - } - - JsonObject jsonInput = getJsonBodyForProxyPUT(req, input); - - if (proxyProps.camelToSnakeCaseConversion) { - jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); - } - - Map headers = getHeadersForProxy(req, input); - - try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - HttpRequest.Response response = OAuth.handleOAuthProxyJsonPUT(main, appIdentifier, storage, proxyProps.path, queryParams, proxyProps.proxyToAdmin, jsonInput, headers); - - if (proxyProps.camelToSnakeCaseConversion) { - response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); - } - - handleResponseFromProxyPUT(req, resp, input, response.statusCode, response.headers, response.rawResponse, response.jsonResponse); - - } catch (OAuthClientNotFoundException e) { - handleOAuthClientNotFoundException(resp); - } catch (OAuthAPIException e) { - handleOAuthAPIException(resp, e); - } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException | BadPermissionException e) { - throw new ServletException(e); - } - } - - private void handleOAuthClientNotFoundException(HttpServletResponse resp) throws IOException { - JsonObject response = new JsonObject(); - response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); - this.sendJsonResponse(200, response, resp); - } - - private void handleOAuthAPIException(HttpServletResponse resp, OAuthAPIException e) throws IOException { - JsonObject response = new JsonObject(); - response.addProperty("status", "OAUTH_ERROR"); - response.addProperty("error", e.error); - if (e.errorDebug != null) { - response.addProperty("errorDebug", e.errorDebug); - } - if (e.errorDescription != null) { - response.addProperty("errorDescription", e.errorDescription); - } - if (e.errorHint != null) { - response.addProperty("errorHint", e.errorHint); - } - response.addProperty("statusCode", e.statusCode); - this.sendJsonResponse(200, response, resp); - } - - protected Map getQueryParamsForProxy(HttpServletRequest req, JsonObject input) throws IOException, ServletException { - Map queryParams = new HashMap<>(); - for (Map.Entry entry : req.getParameterMap().entrySet()) { - queryParams.put(entry.getKey(), entry.getValue()[0]); - } - return queryParams; - } - - protected Map getHeadersForProxy(HttpServletRequest req, JsonObject input) throws IOException, ServletException { - return null; - } - - protected Map getFormFieldsForProxyPOST(HttpServletRequest req, JsonObject input) throws IOException, ServletException { - return null; - } - - protected JsonObject getJsonBodyForProxyPOST(HttpServletRequest req, JsonObject input) throws IOException, ServletException { - return input; - } - - protected JsonObject getJsonBodyForProxyPUT(HttpServletRequest req, JsonObject input) throws IOException, ServletException { - return input; - } - - protected JsonObject getJsonBodyForProxyDELETE(HttpServletRequest req, JsonObject input) throws IOException, ServletException { - return input; - } - - protected void handleResponseFromProxyGET(HttpServletRequest req, HttpServletResponse resp, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - throw new IllegalStateException("Not implemented"); - } - - protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - throw new IllegalStateException("Not implemented"); - } - - protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - throw new IllegalStateException("Not implemented"); - } - - protected void handleResponseFromProxyDELETE(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - throw new IllegalStateException("Not implemented"); - } - - public static class ProxyProps { - public final String apiMethod; - public final String method; - public final String path; - public final boolean proxyToAdmin; - public final boolean camelToSnakeCaseConversion; - - public ProxyProps(String apiMethod, String method, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion) { - this.apiMethod = apiMethod; - this.method = method; - this.path = path; - this.proxyToAdmin = proxyToAdmin; - this.camelToSnakeCaseConversion = camelToSnakeCaseConversion; - } - } -} 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..bc652fec3 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java @@ -0,0 +1,284 @@ +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.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.oauth.HttpRequest; +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 void proxyGET(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + GetQueryParamsForProxy getQueryParamsForProxy, GetHeadersForProxy getHeadersForProxy, + HandleResponse handleResponse) throws IOException, ServletException { + Map queryParams = getQueryParamsForProxy.apply(); + + if (camelToSnakeCaseConversion) { + queryParams = OAuth.convertCamelToSnakeCase(queryParams); + } + + Map headers = getHeadersForProxy.apply(); + + try { + HttpRequest.Response response = OAuth.handleOAuthProxyGET(main, appIdentifier, storage, path, proxyToAdmin, queryParams, headers); + + if (camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + + handleResponse.apply( + response.statusCode, + response.headers, + response.rawResponse, + response.jsonResponse + ); + + } catch (OAuthClientNotFoundException e) { + handleOAuthClientNotFoundException(resp); + } catch (OAuthAPIException e) { + handleOAuthAPIException(resp, e); + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + } + + public static void proxyFormPOST(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + GetFormFieldsForProxy getFormFieldsForProxy, GetHeadersForProxy getHeadersForProxy, + HandleResponse handleResponse) throws IOException, ServletException { + Map formFields = getFormFieldsForProxy.apply(); + + if (camelToSnakeCaseConversion) { + formFields = OAuth.convertCamelToSnakeCase(formFields); + } + + Map headers = getHeadersForProxy.apply(); + + try { + HttpRequest.Response response = OAuth.handleOAuthProxyFormPOST(main, appIdentifier, storage, path, proxyToAdmin, formFields, headers); + + if (camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + + handleResponse.apply( + response.statusCode, + response.headers, + response.rawResponse, + response.jsonResponse + ); + + } catch (OAuthClientNotFoundException e) { + handleOAuthClientNotFoundException(resp); + } catch (OAuthAPIException e) { + handleOAuthAPIException(resp, e); + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + } + + public static void proxyJsonPOST(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + GetJsonBody getJsonBody, GetHeadersForProxy getHeadersForProxy, + HandleResponse handleResponse) throws IOException, ServletException { + JsonObject jsonInput = getJsonBody.apply(); + + if (camelToSnakeCaseConversion) { + jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); + } + + Map headers = getHeadersForProxy.apply(); + + try { + HttpRequest.Response response = OAuth.handleOAuthProxyJsonPOST(main, appIdentifier, storage, path, proxyToAdmin, jsonInput, headers); + + if (camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + + handleResponse.apply( + response.statusCode, + response.headers, + response.rawResponse, + response.jsonResponse + ); + + } catch (OAuthClientNotFoundException e) { + handleOAuthClientNotFoundException(resp); + } catch (OAuthAPIException e) { + handleOAuthAPIException(resp, e); + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + } + + public static void proxyJsonPUT(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + GetQueryParamsForProxy getQueryParamsForProxy, GetJsonBody getJsonBodyForProxyPUT, + GetHeadersForProxy getHeadersForProxy, HandleResponse handleResponse) throws IOException, ServletException { + Map queryParams = getQueryParamsForProxy.apply(); + + if (camelToSnakeCaseConversion) { + queryParams = OAuth.convertCamelToSnakeCase(queryParams); + } + + JsonObject jsonInput = getJsonBodyForProxyPUT.apply(); + + if (camelToSnakeCaseConversion) { + jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); + } + + Map headers = getHeadersForProxy.apply(); + + try { + HttpRequest.Response response = OAuth.handleOAuthProxyJsonPUT(main, appIdentifier, storage, path, queryParams, proxyToAdmin, jsonInput, headers); + + if (camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + + handleResponse.apply( + response.statusCode, + response.headers, + response.rawResponse, + response.jsonResponse + ); + + } catch (OAuthClientNotFoundException e) { + handleOAuthClientNotFoundException(resp); + } catch (OAuthAPIException e) { + handleOAuthAPIException(resp, e); + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + } + + public static void proxyJsonDELETE(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + GetJsonBody getJsonBodyForProxyDELETE, GetHeadersForProxy getHeadersForProxy, + HandleResponse handleResponse) throws IOException, ServletException { + JsonObject jsonInput = getJsonBodyForProxyDELETE.apply(); + + if (camelToSnakeCaseConversion) { + jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); + } + + Map headers = getHeadersForProxy.apply(); + + try { + HttpRequest.Response response = OAuth.handleOAuthProxyJsonDELETE(main, appIdentifier, storage, path, proxyToAdmin, jsonInput, headers); + + if (camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + + handleResponse.apply( + response.statusCode, + response.headers, + response.rawResponse, + response.jsonResponse + ); + + } catch (OAuthClientNotFoundException e) { + handleOAuthClientNotFoundException(resp); + } catch (OAuthAPIException e) { + handleOAuthAPIException(resp, e); + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + } + + 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; + } + + @FunctionalInterface + public interface GetQueryParamsForProxy { + Map apply() throws IOException, ServletException; + } + + @FunctionalInterface + public interface GetFormFieldsForProxy { + Map apply() throws IOException, ServletException; + } + + @FunctionalInterface + public interface GetJsonBody { + JsonObject apply() throws IOException, ServletException; + } + + @FunctionalInterface + public interface GetHeadersForProxy { + Map apply() throws IOException, ServletException; + } + + @FunctionalInterface + public interface HandleResponse { + void apply(int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException; + } + + 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()); + } + + private static void handleOAuthAPIException(HttpServletResponse resp, OAuthAPIException e) throws IOException { + JsonObject response = new JsonObject(); + response.addProperty("status", "OAUTH_ERROR"); + response.addProperty("error", e.error); + if (e.errorDebug != null) { + response.addProperty("errorDebug", e.errorDebug); + } + if (e.errorDescription != null) { + response.addProperty("errorDescription", e.errorDescription); + } + if (e.errorHint != null) { + response.addProperty("errorHint", e.errorHint); + } + 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 index 26fa0311b..ddbd04797 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java @@ -1,21 +1,27 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.util.List; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +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 OAuthProxyBase { +public class OAuthRejectAuthConsentRequestAPI extends WebserverAPI { public OAuthRejectAuthConsentRequestAPI(Main main) { - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override @@ -24,22 +30,28 @@ public String getPath() { } @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { - return new ProxyProps[] { - new ProxyProps( - "PUT", // apiMethod - "PUT_JSON", // method - "/admin/oauth2/auth/requests/consent/reject", // path + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + try { + OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/oauth2/auth/requests/consent/reject", // proxyPath true, // proxyToAdmin - true // camelToSnakeCaseConversion - ) - }; - } - - @Override - protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + true, // camelToSnakeCaseConversion + () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + () -> input, // getJsonBody + HashMap::new, // getHeadersForProxy + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, 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 index 78c40b8f7..4497e5395 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java @@ -1,21 +1,25 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.util.List; -import java.util.Map; +import java.util.HashMap; + -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +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 OAuthProxyBase { +public class OAuthRejectAuthLoginRequestAPI extends WebserverAPI { public OAuthRejectAuthLoginRequestAPI(Main main) { - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override @@ -24,22 +28,28 @@ public String getPath() { } @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { - return new ProxyProps[] { - new ProxyProps( - "PUT", // apiMethod - "PUT_JSON", // method - "/admin/oauth2/auth/requests/login/reject", // path + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + try { + OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/oauth2/auth/requests/login/reject", // proxyPath true, // proxyToAdmin - true // camelToSnakeCaseConversion - ) - }; - } - - @Override - protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + true, // camelToSnakeCaseConversion + () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + () -> input, // getJsonBody + HashMap::new, // getHeadersForProxy + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, 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 index 3f89171a5..326799e03 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java @@ -1,21 +1,24 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.util.List; -import java.util.Map; +import java.util.HashMap; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +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 OAuthProxyBase { +public class OAuthRejectAuthLogoutRequestAPI extends WebserverAPI { public OAuthRejectAuthLogoutRequestAPI(Main main) { - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override @@ -24,22 +27,28 @@ public String getPath() { } @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { - return new ProxyProps[] { - new ProxyProps( - "PUT", // apiMethod - "PUT_JSON", // method - "/admin/oauth2/auth/requests/logout/reject", // path + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + try { + OAuthProxyHelper.proxyJsonPUT( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/oauth2/auth/requests/logout/reject", // proxyPath true, // proxyToAdmin - true // camelToSnakeCaseConversion - ) - }; - } - - @Override - protected void handleResponseFromProxyPUT(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + true, // camelToSnakeCaseConversion + () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + () -> input, // getJsonBody + HashMap::new, // getHeadersForProxy + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + JsonObject response = jsonBody.getAsJsonObject(); + response.addProperty("status", "OK"); + sendJsonResponse(200, response, 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 index 5e1c72bf1..d4157a95f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -22,6 +22,7 @@ import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; 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.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -30,6 +31,7 @@ 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; @@ -42,10 +44,10 @@ import java.util.List; import java.util.Map; -public class OAuthTokenAPI extends OAuthProxyBase { +public class OAuthTokenAPI extends WebserverAPI { public OAuthTokenAPI(Main main) { - super(main); + super(main, RECIPE_ID.OAUTH.toString()); } @Override @@ -54,54 +56,54 @@ public String getPath() { } @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) { - return new ProxyProps[] { - new ProxyProps( - "POST", // apiMethod - "POST_FORM", // method - "/oauth2/token", // path - false, // proxyToAdmin - false // camelToSnakeCaseConversion - ) - }; - } - - @Override - protected Map getFormFieldsForProxyPOST(HttpServletRequest req, JsonObject input) throws IOException, ServletException { - InputParser.parseStringOrThrowError(input, "iss", false); // input validation - + 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, "body", false); - Map formFields = new HashMap<>(); - for (Map.Entry entry : bodyFromSDK.entrySet()) { - formFields.put(entry.getKey(), entry.getValue().getAsString()); - } - - return formFields; - } - - @Override - protected void handleResponseFromProxyPOST(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { - if (jsonBody == null) { - throw new IllegalStateException("unexpected response from hydra"); - } - - String iss = InputParser.parseStringOrThrowError(input, "iss", false); - boolean useDynamicKey = false; Boolean useStaticKeyInput = InputParser.parseBooleanOrThrowError(input, "useStaticSigningKey", true); - // useStaticKeyInput defaults to true, so we check if it has been explicitly set to false - useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - jsonBody = OAuth.transformTokens(super.main, appIdentifier, storage, jsonBody.getAsJsonObject(), iss, useDynamicKey); - - } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { + OAuthProxyHelper.proxyFormPOST( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/oauth2/token", // proxyPath + false, // proxyToAdmin + false, // camelToSnakeCaseConversion + () -> { + Map formFields = new HashMap<>(); + for (Map.Entry entry : bodyFromSDK.entrySet()) { + formFields.put(entry.getKey(), entry.getValue().getAsString()); + } + + return formFields; + }, + HashMap::new, + (statusCode, headers, rawBody, jsonBody) -> { + if (jsonBody == null) { + throw new IllegalStateException("unexpected response from hydra"); + } + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + // useStaticKeyInput defaults to true, so we check if it has been explicitly set to false + boolean useDynamicKey = false; + useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); + jsonBody = OAuth.transformTokens(super.main, appIdentifier, storage, jsonBody.getAsJsonObject(), iss, useDynamicKey); + + } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { + throw new ServletException(e); + } + + jsonBody.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, jsonBody, resp); + } + ); + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); } - - jsonBody.getAsJsonObject().addProperty("status", "OK"); - super.sendJsonResponse(200, jsonBody, resp); } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java index 06e7371ed..9692574e2 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java @@ -17,66 +17,64 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.util.List; -import java.util.Map; +import java.util.HashMap; + -import com.google.gson.JsonElement; 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.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 OAuthProxyBase { - @Override - public String getPath() { - return "/recipe/oauth/clients/remove"; - } +public class RemoveOAuthClientAPI extends WebserverAPI { public RemoveOAuthClientAPI(Main main){ - super(main); - } - - @Override - public ProxyProps[] getProxyProperties(HttpServletRequest req, JsonObject input) throws ServletException { - String clientId = InputParser.parseStringOrThrowError(input, "clientId", false); - - return new ProxyProps[] { - new ProxyProps( - "POST", // apiMethod - "DELETE_JSON", // method - "/admin/clients/" + clientId, // path - true, // proxyToAdmin - true // camelToSnakeCaseConversion - ) - }; + super(main, RECIPE_ID.OAUTH.toString()); } @Override - protected JsonObject getJsonBodyForProxyDELETE(HttpServletRequest req, JsonObject input) - throws IOException, ServletException { - - return new JsonObject(); + public String getPath() { + return "/recipe/oauth/clients/remove"; } @Override - protected void handleResponseFromProxyDELETE(HttpServletRequest req, HttpServletResponse resp, JsonObject input, int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); String clientId = InputParser.parseStringOrThrowError(input, "clientId", false); try { - OAuth.removeClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); - } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + OAuthProxyHelper.proxyJsonDELETE( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/clients/" + clientId, // proxyPath + true, // proxyToAdmin + true, // camelToSnakeCaseConversion + () -> new JsonObject(), // getJsonBody + HashMap::new, // getHeadersForProxy + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + try { + OAuth.removeClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("status", "OK"); + this.sendJsonResponse(200, responseBody, resp); + } + ); + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); } - - JsonObject responseBody = new JsonObject(); - responseBody.addProperty("status", "OK"); - this.sendJsonResponse(200, responseBody, resp); } } From 7f413c603646e530b29fd62f5cf6f53250f8f4eb Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 9 Sep 2024 18:29:51 +0530 Subject: [PATCH 20/40] fix: remove error debug and hint --- src/main/java/io/supertokens/oauth/OAuth.java | 10 +--------- .../java/io/supertokens/oauth/Transformations.java | 4 +--- .../oauth/exceptions/OAuthAPIException.java | 6 +----- .../webserver/api/oauth/OAuthProxyHelper.java | 10 +--------- 4 files changed, 4 insertions(+), 26 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 11ec6fa85..ff58f8471 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -244,19 +244,11 @@ private static void checkNonSuccessResponse(HttpRequest.Response response) throw } if (response.statusCode >= 400) { String error = response.jsonResponse.getAsJsonObject().get("error").getAsString(); - String errorDebug = null; - if (response.jsonResponse.getAsJsonObject().has("error_debug")) { - errorDebug = response.jsonResponse.getAsJsonObject().get("error_debug").getAsString(); - } String errorDescription = null; if (response.jsonResponse.getAsJsonObject().has("error_description")) { errorDescription = response.jsonResponse.getAsJsonObject().get("error_description").getAsString(); } - String errorHint = null; - if (response.jsonResponse.getAsJsonObject().has("error_hint")) { - errorHint = response.jsonResponse.getAsJsonObject().get("error_hint").getAsString(); - } - throw new OAuthAPIException(error, errorDebug, errorDescription, errorHint, response.statusCode); + throw new OAuthAPIException(error, errorDescription, response.statusCode); } } diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index f5979c72d..f15d9166d 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -147,11 +147,9 @@ private static String transformRedirectUrlFromHydra(Main main, AppIdentifier app } } String error = urlQueryParams.getOrDefault("error", null); - String errorDebug = urlQueryParams.getOrDefault("error_debug", null); String errorDescription = urlQueryParams.getOrDefault("error_description", null); - String errorHint = urlQueryParams.getOrDefault("error_hint", null); if (error != null) { - throw new OAuthAPIException(error, errorDebug, errorDescription, errorHint, 400); + throw new OAuthAPIException(error, errorDescription, 400); } redirectTo = redirectTo.replace(hydraInternalAddress, "{apiDomain}"); redirectTo = redirectTo.replace("oauth2/", "oauth/"); diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java index a2cba83f1..dc80be702 100644 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIException.java @@ -20,18 +20,14 @@ public class OAuthAPIException extends Exception { private static final long serialVersionUID = 1836718299845759897L; public final String error; - public final String errorDebug; public final String errorDescription; - public final String errorHint; public final int statusCode; - public OAuthAPIException(String error, String errorDebug, String errorDescription, String errorHint, int statusCode) { + public OAuthAPIException(String error, String errorDescription, int statusCode) { super(error); this.error = error; - this.errorDebug = errorDebug; this.errorDescription = errorDescription; - this.errorHint = errorHint; this.statusCode = statusCode; } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java index bc652fec3..4d67fc97d 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java @@ -266,15 +266,7 @@ private static void handleOAuthAPIException(HttpServletResponse resp, OAuthAPIEx JsonObject response = new JsonObject(); response.addProperty("status", "OAUTH_ERROR"); response.addProperty("error", e.error); - if (e.errorDebug != null) { - response.addProperty("errorDebug", e.errorDebug); - } - if (e.errorDescription != null) { - response.addProperty("errorDescription", e.errorDescription); - } - if (e.errorHint != null) { - response.addProperty("errorHint", e.errorHint); - } + response.addProperty("errorDescription", e.errorDescription); response.addProperty("statusCode", e.statusCode); resp.setStatus(200); From c620cdfb2dfaa625d7a970b41a680baee2573624 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 10 Sep 2024 13:42:44 +0530 Subject: [PATCH 21/40] fix: introspect api --- src/main/java/io/supertokens/oauth/OAuth.java | 24 ++++ .../session/accessToken/AccessToken.java | 40 +++++++ .../io/supertokens/webserver/Webserver.java | 2 + .../webserver/api/oauth/OAuthTokenAPI.java | 1 - .../api/oauth/OAuthTokenIntrospectAPI.java | 107 ++++++++++++++++++ 5 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index ff58f8471..2fd4b3f04 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -23,6 +23,7 @@ import io.supertokens.Main; import io.supertokens.config.Config; +import io.supertokens.exceptions.TryRefreshTokenException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlag; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; @@ -39,6 +40,7 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; +import io.supertokens.session.accessToken.AccessToken; import io.supertokens.session.jwt.JWT.JWTException; import io.supertokens.signingkeys.JWTSigningKey; import io.supertokens.signingkeys.SigningKeys; @@ -381,4 +383,26 @@ public int getValue() { return value; } } + + public static JsonObject introspectAccessToken(Main main, AppIdentifier appIdentifier, Storage storage, + String token) throws StorageQueryException, StorageTransactionLogicException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException { + try { + JsonObject payload = AccessToken.getPayloadFromAccessToken(appIdentifier, main, token); + if (payload.has("stt") && payload.get("stt").getAsInt() == SessionTokenType.ACCESS_TOKEN.value) { + 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 + } + + JsonObject result = new JsonObject(); + result.addProperty("active", false); + return result; + } } diff --git a/src/main/java/io/supertokens/session/accessToken/AccessToken.java b/src/main/java/io/supertokens/session/accessToken/AccessToken.java index 4e316ad28..0b841642c 100644 --- a/src/main/java/io/supertokens/session/accessToken/AccessToken.java +++ b/src/main/java/io/supertokens/session/accessToken/AccessToken.java @@ -56,6 +56,46 @@ public class AccessToken { + public static JsonObject getPayloadFromAccessToken(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; + } + // TODO: device fingerprint - store hash of this in JWT. private static AccessTokenInfo getInfoFromAccessToken(AppIdentifier appIdentifier, diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index 89ead8fe1..da4c134aa 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -52,6 +52,7 @@ import io.supertokens.webserver.api.oauth.OAuthAcceptAuthLoginRequestAPI; import io.supertokens.webserver.api.oauth.OAuthAcceptAuthLogoutRequestAPI; import io.supertokens.webserver.api.oauth.OAuthTokenAPI; +import io.supertokens.webserver.api.oauth.OAuthTokenIntrospectAPI; import io.supertokens.webserver.api.oauth.RemoveOAuthClientAPI; import io.supertokens.webserver.api.passwordless.*; import io.supertokens.webserver.api.session.*; @@ -296,6 +297,7 @@ private void setupRoutes() { addAPI(new OAuthGetAuthLogoutRequestAPI(main)); addAPI(new OAuthAcceptAuthLogoutRequestAPI(main)); addAPI(new OAuthRejectAuthLogoutRequestAPI(main)); + addAPI(new OAuthTokenIntrospectAPI(main)); StandardContext context = tomcatReference.getContext(); Tomcat tomcat = tomcatReference.getTomcat(); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index d4157a95f..60676fd94 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -41,7 +41,6 @@ import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.HashMap; -import java.util.List; import java.util.Map; public class OAuthTokenAPI extends WebserverAPI { 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..77a54f46b --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java @@ -0,0 +1,107 @@ +/* + * 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.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; +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.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_")) { + String iss = InputParser.parseStringOrThrowError(input, "iss", false); + + try { + OAuthProxyHelper.proxyFormPOST( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/oauth2/introspect", + true, + false, + () -> { + Map formFields = new HashMap<>(); + for (Map.Entry entry : input.entrySet()) { + formFields.put(entry.getKey(), entry.getValue().getAsString()); + } + + return formFields; + }, + HashMap::new, + (statusCode, headers, rawBody, jsonBody) -> { + JsonObject jsonObject = jsonBody.getAsJsonObject(); + + jsonObject.addProperty("iss", iss); + if (jsonObject.has("ext")) { + JsonObject ext = jsonObject.get("ext").getAsJsonObject(); + for (Map.Entry entry : ext.entrySet()) { + jsonObject.add(entry.getKey(), entry.getValue()); + } + jsonObject.remove("ext"); + } + + jsonObject.addProperty("status", "OK"); + super.sendJsonResponse(200, jsonBody, 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); + } + } + } +} From 3e944436c5a95a4cf06a0f97925cb77610682d68 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 11 Sep 2024 14:04:03 +0530 Subject: [PATCH 22/40] fix: pr comments --- build.gradle | 3 - config.yaml | 2 +- devConfig.yaml | 7 +- .../java/io/supertokens/config/Config.java | 1 - .../io/supertokens/config/CoreConfig.java | 6 +- .../io/supertokens/oauth/HttpRequest.java | 10 ++- .../io/supertokens/oauth/JWTVerification.java | 88 ------------------- src/main/java/io/supertokens/oauth/OAuth.java | 7 +- src/main/java/io/supertokens/oauth/Utils.java | 25 ------ .../CreateUpdateOrGetOAuthClientAPI.java | 21 +---- .../webserver/api/oauth/OAuthAuthAPI.java | 12 +-- .../webserver/api/oauth/OAuthTokenAPI.java | 3 +- 12 files changed, 23 insertions(+), 162 deletions(-) delete mode 100644 src/main/java/io/supertokens/oauth/JWTVerification.java delete mode 100644 src/main/java/io/supertokens/oauth/Utils.java diff --git a/build.gradle b/build.gradle index ace25d503..b2f1a910d 100644 --- a/build.gradle +++ b/build.gradle @@ -73,9 +73,6 @@ dependencies { // https://mvnrepository.com/artifact/com.googlecode.libphonenumber/libphonenumber/ implementation group: 'com.googlecode.libphonenumber', name: 'libphonenumber', version: '8.13.25' - implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.12.6' - runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.12.6' - runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.12.6' compileOnly project(":supertokens-plugin-interface") testImplementation project(":supertokens-plugin-interface") 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 1b26e46c8..f55e424fe 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -160,11 +160,10 @@ oauth_provider_public_service_url: http://localhost:4444 # service. 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: http://localhost:3000 +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: http://localhost:4001 \ No newline at end of file +oauth_provider_url_configured_in_hydra: http://localhost:4001 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..036aebaf6 100644 --- a/src/main/java/io/supertokens/config/CoreConfig.java +++ b/src/main/java/io/supertokens/config/CoreConfig.java @@ -298,16 +298,14 @@ public class CoreConfig { @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"; + 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/oauth/HttpRequest.java b/src/main/java/io/supertokens/oauth/HttpRequest.java index 3524e1090..ff3b954d7 100644 --- a/src/main/java/io/supertokens/oauth/HttpRequest.java +++ b/src/main/java/io/supertokens/oauth/HttpRequest.java @@ -151,12 +151,14 @@ public static Response doJsonDelete(String url, Map headers, Jso private static Response getResponse(HttpURLConnection con) throws IOException { int responseCode = con.getResponseCode(); - BufferedReader in; - if (responseCode < 400) { - in = new BufferedReader(new InputStreamReader(con.getInputStream())); + InputStream inputStream; + if (con.getErrorStream() != null) { + inputStream = con.getErrorStream(); } else { - in = new BufferedReader(new InputStreamReader(con.getErrorStream())); + inputStream = con.getInputStream(); } + BufferedReader in = new BufferedReader(new InputStreamReader(inputStream)); + String inputLine; StringBuilder response = new StringBuilder(); while ((inputLine = in.readLine()) != null) { diff --git a/src/main/java/io/supertokens/oauth/JWTVerification.java b/src/main/java/io/supertokens/oauth/JWTVerification.java deleted file mode 100644 index 08791d213..000000000 --- a/src/main/java/io/supertokens/oauth/JWTVerification.java +++ /dev/null @@ -1,88 +0,0 @@ -package io.supertokens.oauth; - -import java.security.Key; -import java.util.HashMap; -import java.util.Map; - -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.Jws; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.Locator; -import io.jsonwebtoken.security.Jwk; -import io.jsonwebtoken.security.Jwks; -import io.supertokens.Main; -import io.supertokens.httpRequest.HttpRequest; - -public class JWTVerification { - - public static JsonObject verifyJWTAndGetPayload(Main main, String token, String jwksUrl) { - JwtParser jwtParser = Jwts.parser().keyLocator(new KeyLocatorImpl(main, jwksUrl)).build(); - Jws jwtResult = jwtParser.parseSignedClaims(token); - if (jwtResult == null) { - throw new RuntimeException("Failed to verify JWT token"); - } - Claims payload = jwtResult.getPayload(); - - return new Gson().fromJson(new Gson().toJson(payload), JsonObject.class); - } - - private static class KeyLocatorImpl implements Locator { - private static final int CONNECTION_TIMEOUT = 5000; - private static final int READ_TIMEOUT = 5000; - private static final int MAX_RETRIES = 3; // Maximum number of retries for fetching JWKS - - private static Map jwksCache = new HashMap<>(); // Cache for JWKS keys - - private final Main main; - private final String jwksUrl; - - public KeyLocatorImpl(Main main, String jwksUrl) { - this.main = main; - this.jwksUrl = jwksUrl; - } - - @Override - public Key locate(Header header) { - for (int i = 0; i < MAX_RETRIES; i++) { - JsonObject jwksResponse = jwksCache.get(jwksUrl); - if (jwksResponse == null) { - try { - jwksResponse = HttpRequest - .sendGETRequest(main, "", jwksUrl, null, CONNECTION_TIMEOUT, READ_TIMEOUT, null); - jwksCache.put(jwksUrl, jwksResponse); // Cache the fetched JWKS response - } catch (Exception e) { - throw new RuntimeException("Failed to fetch JWKS keys for token verification", e); - } - } - - Jwk jwk = null; - - JsonArray keys = jwksResponse.get("keys").getAsJsonArray(); - - for (JsonElement keyElement : keys) { - JsonObject keyObject = keyElement.getAsJsonObject(); - if (keyObject.get("kid").getAsString().equals(header.get("kid"))) { - jwk = Jwks.parser().build().parse(keyObject.toString()); - break; - } - } - - if (jwk == null) { - jwksCache.remove(jwksUrl); - continue; // Retry - } - - return jwk.toKey(); - } - - throw new RuntimeException("Failed to fetch JWKS keys for token verification"); - } - } -} diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 2fd4b3f04..954132ca2 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -41,6 +41,7 @@ import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.session.accessToken.AccessToken; +import io.supertokens.session.jwt.JWT; import io.supertokens.session.jwt.JWT.JWTException; import io.supertokens.signingkeys.JWTSigningKey; import io.supertokens.signingkeys.SigningKeys; @@ -278,11 +279,7 @@ public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, private static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, SessionTokenType tokenType, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { // Load the JWKS from the specified URL - String publicOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderPublicServiceUrl(); - String jwksUrl = publicOAuthProviderServiceUrl + HYDRA_JWKS_PATH; - - // Validate the JWT and extract claims using the fetched public signing keys - JsonObject payload = JWTVerification.verifyJWTAndGetPayload(main, token, jwksUrl); + JsonObject payload = JWT.getPayloadWithoutVerifying(token).payload; // move keys in ext to root if (tokenType == SessionTokenType.ACCESS_TOKEN && payload.has("ext")) { diff --git a/src/main/java/io/supertokens/oauth/Utils.java b/src/main/java/io/supertokens/oauth/Utils.java deleted file mode 100644 index ee2fb9fbf..000000000 --- a/src/main/java/io/supertokens/oauth/Utils.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.supertokens.oauth; - -import java.util.HashMap; -import java.util.Map; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - -public class Utils { - public static Map getQueryParamsMapFromJsonObject(JsonObject jsonObject) { - Map queryParams = new HashMap<>(); - for (Map.Entry entry : jsonObject.entrySet()) { - queryParams.put(entry.getKey(), entry.getValue().getAsString()); - } - return queryParams; - } - - public static Map getBodyParamsMapFromJsonObject(JsonObject bodyFromSdk) { - Map bodyParams = new HashMap<>(); - for (Map.Entry entry : bodyFromSdk.entrySet()) { - bodyParams.put(entry.getKey(), entry.getValue().getAsString()); - } - return bodyParams; - } -} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index 174a3e927..171fb388b 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -65,25 +65,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/clients/" + clientId, // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - () -> { // getQueryParamsForProxy - 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; - }, - () -> { // getHeadersForProxy - return new HashMap<>(); - }, + () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + () -> new HashMap<>(), // getHeadersForProxy (statusCode, headers, rawBody, jsonBody) -> { // handleResponse this.sendJsonResponse(200, jsonBody, resp); } 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 1e103700f..6bf9c8a33 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -57,16 +57,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), - "/oauth2/auth", - false, - false, - () -> { + "/oauth2/auth", // proxyPath + false, // proxyToAdmin + false, // camelToSnakeCaseConversion + () -> { // getQueryParamsForProxy return params.entrySet().stream().collect(Collectors.toMap( Map.Entry::getKey, e -> e.getValue().getAsString() )); }, - () -> { + () -> { // getHeadersForProxy Map headers = new HashMap<>(); if (cookies != null) { @@ -75,7 +75,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I return headers; }, - (statusCode, headers, rawBody, jsonBody) -> { + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse if (headers == null || !headers.containsKey("Location")) { throw new IllegalStateException("Invalid response from hydra"); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 60676fd94..6d56e396d 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -89,8 +89,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); // useStaticKeyInput defaults to true, so we check if it has been explicitly set to false - boolean useDynamicKey = false; - useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); + boolean useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); jsonBody = OAuth.transformTokens(super.main, appIdentifier, storage, jsonBody.getAsJsonObject(), iss, useDynamicKey); } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { From b79ddfac4722949f73c6cfcf73e48570fac6189f Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 11 Sep 2024 14:13:43 +0530 Subject: [PATCH 23/40] fix: pr comments --- src/main/java/io/supertokens/oauth/OAuth.java | 52 +------- .../java/io/supertokens/oauth/OAuthToken.java | 119 ++++++++++++++++++ .../OAuthAPIInvalidInputException.java | 29 ----- .../OAuthClientUpdateException.java | 28 ----- .../oauth/exceptions/OAuthException.java | 35 ------ .../session/accessToken/AccessToken.java | 40 ------ 6 files changed, 123 insertions(+), 180 deletions(-) create mode 100644 src/main/java/io/supertokens/oauth/OAuthToken.java delete mode 100644 src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java delete mode 100644 src/main/java/io/supertokens/oauth/exceptions/OAuthClientUpdateException.java delete mode 100644 src/main/java/io/supertokens/oauth/exceptions/OAuthException.java diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 954132ca2..91bc25d90 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -258,13 +258,13 @@ private static void checkNonSuccessResponse(HttpRequest.Response response) throw public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject jsonBody, String iss, boolean useDynamicKey) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { if (jsonBody.has("access_token")) { String accessToken = jsonBody.get("access_token").getAsString(); - accessToken = reSignToken(appIdentifier, main, accessToken, iss, SessionTokenType.ACCESS_TOKEN, useDynamicKey, 0); + accessToken = OAuthToken.reSignToken(appIdentifier, main, accessToken, iss, OAuthToken.TokenType.ACCESS_TOKEN, useDynamicKey, 0); jsonBody.addProperty("access_token", accessToken); } if (jsonBody.has("id_token")) { String idToken = jsonBody.get("id_token").getAsString(); - idToken = reSignToken(appIdentifier, main, idToken, iss, SessionTokenType.ID_TOKEN, useDynamicKey, 0); + idToken = OAuthToken.reSignToken(appIdentifier, main, idToken, iss, OAuthToken.TokenType.ID_TOKEN, useDynamicKey, 0); jsonBody.addProperty("id_token", idToken); } @@ -277,35 +277,6 @@ public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, return jsonBody; } - private static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, SessionTokenType tokenType, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { - // Load the JWKS from the specified URL - JsonObject payload = JWT.getPayloadWithoutVerifying(token).payload; - - // move keys in ext to root - if (tokenType == SessionTokenType.ACCESS_TOKEN && payload.has("ext")) { - JsonObject ext = payload.getAsJsonObject("ext"); - for (Map.Entry entry : ext.entrySet()) { - payload.add(entry.getKey(), entry.getValue()); - } - payload.remove("ext"); - } - payload.addProperty("iss", iss); - payload.addProperty("stt", tokenType.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; - } - public static void addClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); oauthStorage.addClientForApp(appIdentifier, clientId); @@ -366,26 +337,11 @@ public static JsonElement convertSnakeCaseToCamelCaseRecursively(JsonElement jso } - public static enum SessionTokenType { - ACCESS_TOKEN(1), - ID_TOKEN(2); - - private final int value; - - SessionTokenType(int value) { - this.value = value; - } - - public int getValue() { - return value; - } - } - public static JsonObject introspectAccessToken(Main main, AppIdentifier appIdentifier, Storage storage, String token) throws StorageQueryException, StorageTransactionLogicException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException { try { - JsonObject payload = AccessToken.getPayloadFromAccessToken(appIdentifier, main, token); - if (payload.has("stt") && payload.get("stt").getAsInt() == SessionTokenType.ACCESS_TOKEN.value) { + JsonObject payload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, token); + if (payload.has("stt") && payload.get("stt").getAsInt() == OAuthToken.TokenType.ACCESS_TOKEN.getValue()) { payload.addProperty("active", true); payload.addProperty("token_type", "Bearer"); payload.addProperty("token_use", "access_token"); 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..4c41e65d0 --- /dev/null +++ b/src/main/java/io/supertokens/oauth/OAuthToken.java @@ -0,0 +1,119 @@ +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; + +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; + } + } + + 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, TokenType tokenType, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, + JWTCreationException { + // Load the JWKS from the specified URL + JsonObject payload = JWT.getPayloadWithoutVerifying(token).payload; + + // move keys in ext to root + if (tokenType == TokenType.ACCESS_TOKEN && payload.has("ext")) { + JsonObject ext = payload.getAsJsonObject("ext"); + for (Map.Entry entry : ext.entrySet()) { + payload.add(entry.getKey(), entry.getValue()); + } + payload.remove("ext"); + } + payload.addProperty("iss", iss); + payload.addProperty("stt", tokenType.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/exceptions/OAuthAPIInvalidInputException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java deleted file mode 100644 index e84f89611..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, 400); - } -} 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 a86aa3a27..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, int statusCode) { - super(error, errorDescription, statusCode); - } -} diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthException.java deleted file mode 100644 index fe8252391..000000000 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthException.java +++ /dev/null @@ -1,35 +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 OAuthException extends Exception{ - @Serial - private static final long serialVersionUID = 1836718299845759897L; - - public final String error; - public final String errorDescription; - public final int statusCode; - - public OAuthException(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/session/accessToken/AccessToken.java b/src/main/java/io/supertokens/session/accessToken/AccessToken.java index 0b841642c..4e316ad28 100644 --- a/src/main/java/io/supertokens/session/accessToken/AccessToken.java +++ b/src/main/java/io/supertokens/session/accessToken/AccessToken.java @@ -56,46 +56,6 @@ public class AccessToken { - public static JsonObject getPayloadFromAccessToken(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; - } - // TODO: device fingerprint - store hash of this in JWT. private static AccessTokenInfo getInfoFromAccessToken(AppIdentifier appIdentifier, From ca95c13bbecff3412c07caa03357d39f24c220f2 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 11 Sep 2024 15:48:16 +0530 Subject: [PATCH 24/40] fix: pr comment --- .../java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 6d56e396d..2fdb846a9 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -58,7 +58,7 @@ public String getPath() { 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, "body", false); + JsonObject bodyFromSDK = InputParser.parseJsonObjectOrThrowError(input, "inputBody", false); Boolean useStaticKeyInput = InputParser.parseBooleanOrThrowError(input, "useStaticSigningKey", true); From 621befb63291b631a9c6bd7a02343f5d138bd014 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 11 Sep 2024 16:16:03 +0530 Subject: [PATCH 25/40] fix: pr comment --- src/main/java/io/supertokens/oauth/OAuth.java | 6 +++--- .../java/io/supertokens/oauth/OAuthToken.java | 17 ++++++++++++++++- .../webserver/api/oauth/OAuthTokenAPI.java | 7 +++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 91bc25d90..ede17b745 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -255,16 +255,16 @@ private static void checkNonSuccessResponse(HttpRequest.Response response) throw } } - public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject jsonBody, String iss, boolean useDynamicKey) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { + 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 { if (jsonBody.has("access_token")) { String accessToken = jsonBody.get("access_token").getAsString(); - accessToken = OAuthToken.reSignToken(appIdentifier, main, accessToken, iss, OAuthToken.TokenType.ACCESS_TOKEN, useDynamicKey, 0); + accessToken = OAuthToken.reSignToken(appIdentifier, main, accessToken, iss, accessTokenUpdate, OAuthToken.TokenType.ACCESS_TOKEN, useDynamicKey, 0); jsonBody.addProperty("access_token", accessToken); } if (jsonBody.has("id_token")) { String idToken = jsonBody.get("id_token").getAsString(); - idToken = OAuthToken.reSignToken(appIdentifier, main, idToken, iss, OAuthToken.TokenType.ID_TOKEN, useDynamicKey, 0); + idToken = OAuthToken.reSignToken(appIdentifier, main, idToken, iss, idTokenUpdate, OAuthToken.TokenType.ID_TOKEN, useDynamicKey, 0); jsonBody.addProperty("id_token", idToken); } diff --git a/src/main/java/io/supertokens/oauth/OAuthToken.java b/src/main/java/io/supertokens/oauth/OAuthToken.java index 4c41e65d0..61d4db15c 100644 --- a/src/main/java/io/supertokens/oauth/OAuthToken.java +++ b/src/main/java/io/supertokens/oauth/OAuthToken.java @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; public class OAuthToken { public enum TokenType { @@ -45,6 +46,14 @@ public int getValue() { } } + private static Set NON_OVERRIDABLE_TOKEN_PROPS = Set.of( + "kid", "typ", "alg", "aud", + "iss", "iat", "exp", "nbf", "jti", "ext", + "sid", "rat", "at_hash", + "client_id", "scp", "sub", "rsub", + "sessionHandle", "tId", "stt" + ); + public static JsonObject getPayloadFromJWTToken(AppIdentifier appIdentifier, @Nonnull Main main, @Nonnull String token) throws TenantOrAppNotFoundException, TryRefreshTokenException, StorageQueryException, @@ -87,7 +96,7 @@ public static JsonObject getPayloadFromJWTToken(AppIdentifier appIdentifier, return jwtInfo.payload; } - public static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, TokenType tokenType, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, + public static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, JsonObject payloadUpdate, TokenType tokenType, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException { // Load the JWKS from the specified URL JsonObject payload = JWT.getPayloadWithoutVerifying(token).payload; @@ -103,6 +112,12 @@ public static String reSignToken(AppIdentifier appIdentifier, Main main, String payload.addProperty("iss", iss); payload.addProperty("stt", tokenType.getValue()); + 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( diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 2fdb846a9..41cd057af 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -88,14 +88,17 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(bodyFromSDK, "access_token", true); + JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(bodyFromSDK, "id_token", true); + // useStaticKeyInput defaults to true, so we check if it has been explicitly set to false boolean useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); - jsonBody = OAuth.transformTokens(super.main, appIdentifier, storage, jsonBody.getAsJsonObject(), iss, useDynamicKey); + jsonBody = OAuth.transformTokens(super.main, appIdentifier, storage, jsonBody.getAsJsonObject(), iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { throw new ServletException(e); } - + jsonBody.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, jsonBody, resp); } From 12c09a08d6ccf8f324e6149d72aff6d9c9591f79 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 12 Sep 2024 12:08:14 +0530 Subject: [PATCH 26/40] fix: pr comments and refactor --- src/main/java/io/supertokens/oauth/OAuth.java | 8 -- .../java/io/supertokens/oauth/OAuthToken.java | 1 - .../CreateUpdateOrGetOAuthClientAPI.java | 75 ++++++++----------- .../OAuthAcceptAuthConsentRequestAPI.java | 9 +-- .../oauth/OAuthAcceptAuthLoginRequestAPI.java | 9 +-- .../OAuthAcceptAuthLogoutRequestAPI.java | 9 +-- .../webserver/api/oauth/OAuthAuthAPI.java | 36 +++++---- .../api/oauth/OAuthClientListAPI.java | 4 +- .../oauth/OAuthGetAuthConsentRequestAPI.java | 7 +- .../oauth/OAuthGetAuthLoginRequestAPI.java | 7 +- .../oauth/OAuthGetAuthLogoutRequestAPI.java | 7 +- .../webserver/api/oauth/OAuthProxyHelper.java | 54 ++----------- .../OAuthRejectAuthConsentRequestAPI.java | 9 +-- .../oauth/OAuthRejectAuthLoginRequestAPI.java | 6 +- .../OAuthRejectAuthLogoutRequestAPI.java | 6 +- .../webserver/api/oauth/OAuthTokenAPI.java | 20 +++-- .../api/oauth/OAuthTokenIntrospectAPI.java | 24 +++--- .../api/oauth/RemoveOAuthClientAPI.java | 4 +- 18 files changed, 103 insertions(+), 192 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index ede17b745..18b9e8fea 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -27,7 +27,6 @@ import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlag; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; -import io.supertokens.jwt.JWTSigningFunctions; import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; import io.supertokens.oauth.exceptions.*; import io.supertokens.pluginInterface.Storage; @@ -35,16 +34,11 @@ import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; -import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo; 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.accessToken.AccessToken; -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 java.io.IOException; @@ -55,8 +49,6 @@ import java.util.Map.Entry; public class OAuth { - private static final String HYDRA_JWKS_PATH = "/.well-known/jwks.json"; - private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException { EE_FEATURES[] features = FeatureFlag.getInstance(main, appIdentifier).getEnabledFeatures(); diff --git a/src/main/java/io/supertokens/oauth/OAuthToken.java b/src/main/java/io/supertokens/oauth/OAuthToken.java index 61d4db15c..ec4cd5e21 100644 --- a/src/main/java/io/supertokens/oauth/OAuthToken.java +++ b/src/main/java/io/supertokens/oauth/OAuthToken.java @@ -98,7 +98,6 @@ public static JsonObject getPayloadFromJWTToken(AppIdentifier appIdentifier, public static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, JsonObject payloadUpdate, TokenType tokenType, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException { - // Load the JWKS from the specified URL JsonObject payload = JWT.getPayloadWithoutVerifying(token).payload; // move keys in ext to root diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index 171fb388b..60fc8f956 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -17,8 +17,6 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -65,8 +63,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/clients/" + clientId, // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - () -> new HashMap<>(), // getHeadersForProxy + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + new HashMap<>(), // getHeadersForProxy (statusCode, headers, rawBody, jsonBody) -> { // handleResponse this.sendJsonResponse(200, jsonBody, resp); } @@ -93,12 +91,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I "/admin/clients", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - () -> { // getJsonBody - return input; - }, - () -> { // getHeadersForProxy - return new HashMap<>(); - }, + input, // jsonBody + new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { // handleResponse String clientId = jsonBody.getAsJsonObject().get("clientId").getAsString(); @@ -122,6 +116,31 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject input = InputParser.parseJsonObjectOrThrowError(req); String clientId = InputParser.parseStringOrThrowError(input, "clientId", false); + // Apply existing client config on top of input + try { + Map queryParams = new HashMap<>(); + queryParams.put("client_id", clientId); + HttpRequest.Response response = OAuth.handleOAuthProxyGET( + main, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/clients/" + clientId, + true, queryParams, null); + + JsonObject existingConfig = response.jsonResponse.getAsJsonObject(); + existingConfig = OAuth.convertSnakeCaseToCamelCaseRecursively(existingConfig).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 { OAuthProxyHelper.proxyJsonPUT( main, req, resp, @@ -130,39 +149,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/clients/" + clientId, true, // proxyToAdmin true, // camelToSnakeCaseConversion - () -> { // getJsonBody - return new HashMap<>(); - }, - () -> { // getHeadersForProxy - try { - Map queryParams = new HashMap<>(); - queryParams.put("client_id", clientId); - HttpRequest.Response response = OAuth.handleOAuthProxyGET( - main, - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), - "/admin/clients/" + clientId, - true, queryParams, null); - - JsonObject existingConfig = response.jsonResponse.getAsJsonObject(); - existingConfig = OAuth.convertSnakeCaseToCamelCaseRecursively(existingConfig).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 - } - - return input; - }, - () -> { // getHeadersForProxy - return new HashMap<>(); - }, + new HashMap<>(), // queryParams + input, // jsonBody + new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { // handleResponse this.sendJsonResponse(200, jsonBody, resp); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java index 17cdb45c1..9532543ae 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java @@ -1,10 +1,7 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.Map; import com.google.gson.JsonObject; @@ -41,9 +38,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/oauth2/auth/requests/consent/accept", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - () -> input, // getJsonBody - HashMap::new, // getHeadersForProxy + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + input, // jsonBody + new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java index ac76dd2f0..3dfd9a07a 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java @@ -1,10 +1,7 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.Map; import com.google.gson.JsonObject; @@ -41,9 +38,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/oauth2/auth/requests/login/accept", true, true, - () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - () -> input, - HashMap::new, + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + input, // jsonBody + new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); 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 ec6586d2d..0a93ad6c7 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java @@ -1,10 +1,7 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.Map; import com.google.gson.JsonObject; @@ -41,9 +38,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/oauth2/auth/requests/logout/accept", true, true, - () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - () -> input, - HashMap::new, + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + input, + new HashMap<>(), (statusCode, headers, rawBody, jsonBody) -> { JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); 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 6bf9c8a33..0eb2df1e5 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -52,6 +52,17 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject params = InputParser.parseJsonObjectOrThrowError(input, "params", false); String cookies = InputParser.parseStringOrThrowError(input, "cookies", true); + 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 { OAuthProxyHelper.proxyGET( main, req, resp, @@ -60,28 +71,15 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I "/oauth2/auth", // proxyPath false, // proxyToAdmin false, // camelToSnakeCaseConversion - () -> { // getQueryParamsForProxy - return params.entrySet().stream().collect(Collectors.toMap( - Map.Entry::getKey, - e -> e.getValue().getAsString() - )); - }, - () -> { // getHeadersForProxy - Map headers = new HashMap<>(); - - if (cookies != null) { - headers.put("Cookie", cookies); - } - - return headers; - }, - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse - if (headers == null || !headers.containsKey("Location")) { + queryParams, + headers, + (statusCode, responseHeaders, rawBody, jsonBody) -> { // handleResponse + if (headers == null || !responseHeaders.containsKey("Location")) { throw new IllegalStateException("Invalid response from hydra"); } - String redirectTo = headers.get("Location").get(0); - List responseCookies = headers.get("Set-Cookie"); + String redirectTo = responseHeaders.get("Location").get(0); + List responseCookies = responseHeaders.get("Set-Cookie"); JsonObject response = new JsonObject(); response.addProperty("redirectTo", redirectTo); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java index 9fbdd8085..d9fc6a46f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java @@ -42,8 +42,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/clients", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - HashMap::new, // getQueryParamsForProxy - HashMap::new, // getHeadersForProxy + new HashMap<>(), // queryParams + new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = new JsonObject(); response.addProperty("status", "OK"); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java index 371a0f0b1..0c1b7f632 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java @@ -1,10 +1,7 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.Map; import com.google.gson.JsonObject; @@ -38,8 +35,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/oauth2/auth/requests/consent", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - HashMap::new, // getHeadersForProxy + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java index 0c848d7ca..9cadc4708 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java @@ -1,10 +1,7 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.Map; import com.google.gson.JsonObject; @@ -38,8 +35,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/oauth2/auth/requests/login", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - HashMap::new, // getHeadersForProxy + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java index 3afa294d7..0349bbce4 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java @@ -1,10 +1,7 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.Map; import com.google.gson.JsonObject; @@ -38,8 +35,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/oauth2/auth/requests/logout", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - HashMap::new, // getHeadersForProxy + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java index 4d67fc97d..dff334f77 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java @@ -32,16 +32,12 @@ public class OAuthProxyHelper { public static void proxyGET(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, - GetQueryParamsForProxy getQueryParamsForProxy, GetHeadersForProxy getHeadersForProxy, + Map queryParams, Map headers, HandleResponse handleResponse) throws IOException, ServletException { - Map queryParams = getQueryParamsForProxy.apply(); - if (camelToSnakeCaseConversion) { queryParams = OAuth.convertCamelToSnakeCase(queryParams); } - Map headers = getHeadersForProxy.apply(); - try { HttpRequest.Response response = OAuth.handleOAuthProxyGET(main, appIdentifier, storage, path, proxyToAdmin, queryParams, headers); @@ -67,16 +63,12 @@ public static void proxyGET(Main main, HttpServletRequest req, HttpServletRespon public static void proxyFormPOST(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, - GetFormFieldsForProxy getFormFieldsForProxy, GetHeadersForProxy getHeadersForProxy, + Map formFields, Map headers, HandleResponse handleResponse) throws IOException, ServletException { - Map formFields = getFormFieldsForProxy.apply(); - if (camelToSnakeCaseConversion) { formFields = OAuth.convertCamelToSnakeCase(formFields); } - Map headers = getHeadersForProxy.apply(); - try { HttpRequest.Response response = OAuth.handleOAuthProxyFormPOST(main, appIdentifier, storage, path, proxyToAdmin, formFields, headers); @@ -102,16 +94,12 @@ public static void proxyFormPOST(Main main, HttpServletRequest req, HttpServletR public static void proxyJsonPOST(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, - GetJsonBody getJsonBody, GetHeadersForProxy getHeadersForProxy, + JsonObject jsonInput, Map headers, HandleResponse handleResponse) throws IOException, ServletException { - JsonObject jsonInput = getJsonBody.apply(); - if (camelToSnakeCaseConversion) { jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); } - Map headers = getHeadersForProxy.apply(); - try { HttpRequest.Response response = OAuth.handleOAuthProxyJsonPOST(main, appIdentifier, storage, path, proxyToAdmin, jsonInput, headers); @@ -137,22 +125,16 @@ public static void proxyJsonPOST(Main main, HttpServletRequest req, HttpServletR public static void proxyJsonPUT(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, - GetQueryParamsForProxy getQueryParamsForProxy, GetJsonBody getJsonBodyForProxyPUT, - GetHeadersForProxy getHeadersForProxy, HandleResponse handleResponse) throws IOException, ServletException { - Map queryParams = getQueryParamsForProxy.apply(); - + Map queryParams, JsonObject jsonInput, + Map headers, HandleResponse handleResponse) throws IOException, ServletException { if (camelToSnakeCaseConversion) { queryParams = OAuth.convertCamelToSnakeCase(queryParams); } - JsonObject jsonInput = getJsonBodyForProxyPUT.apply(); - if (camelToSnakeCaseConversion) { jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); } - Map headers = getHeadersForProxy.apply(); - try { HttpRequest.Response response = OAuth.handleOAuthProxyJsonPUT(main, appIdentifier, storage, path, queryParams, proxyToAdmin, jsonInput, headers); @@ -178,16 +160,12 @@ public static void proxyJsonPUT(Main main, HttpServletRequest req, HttpServletRe public static void proxyJsonDELETE(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, - GetJsonBody getJsonBodyForProxyDELETE, GetHeadersForProxy getHeadersForProxy, + JsonObject jsonInput, Map headers, HandleResponse handleResponse) throws IOException, ServletException { - JsonObject jsonInput = getJsonBodyForProxyDELETE.apply(); - if (camelToSnakeCaseConversion) { jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); } - Map headers = getHeadersForProxy.apply(); - try { HttpRequest.Response response = OAuth.handleOAuthProxyJsonDELETE(main, appIdentifier, storage, path, proxyToAdmin, jsonInput, headers); @@ -228,26 +206,6 @@ public static Map defaultGetQueryParamsFromRequest(HttpServletRe return queryParams; } - @FunctionalInterface - public interface GetQueryParamsForProxy { - Map apply() throws IOException, ServletException; - } - - @FunctionalInterface - public interface GetFormFieldsForProxy { - Map apply() throws IOException, ServletException; - } - - @FunctionalInterface - public interface GetJsonBody { - JsonObject apply() throws IOException, ServletException; - } - - @FunctionalInterface - public interface GetHeadersForProxy { - Map apply() throws IOException, ServletException; - } - @FunctionalInterface public interface HandleResponse { void apply(int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException; diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java index ddbd04797..816229714 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java @@ -1,10 +1,7 @@ package io.supertokens.webserver.api.oauth; import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.Map; import com.google.gson.JsonObject; @@ -41,9 +38,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/oauth2/auth/requests/consent/reject", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - () -> input, // getJsonBody - HashMap::new, // getHeadersForProxy + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + input, // getJsonBody + new HashMap<>(), // getHeadersForProxy (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java index 4497e5395..aff51a895 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java @@ -39,9 +39,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/oauth2/auth/requests/login/reject", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - () -> input, // getJsonBody - HashMap::new, // getHeadersForProxy + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + input, // jsonBody + new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); 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 326799e03..41311b9d4 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java @@ -38,9 +38,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/oauth2/auth/requests/logout/reject", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - () -> OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - () -> input, // getJsonBody - HashMap::new, // getHeadersForProxy + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + input, // jsonBody + new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 41cd057af..23ee2ead5 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -62,6 +62,11 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I Boolean useStaticKeyInput = InputParser.parseBooleanOrThrowError(input, "useStaticSigningKey", true); + Map formFields = new HashMap<>(); + for (Map.Entry entry : bodyFromSDK.entrySet()) { + formFields.put(entry.getKey(), entry.getValue().getAsString()); + } + try { OAuthProxyHelper.proxyFormPOST( main, req, resp, @@ -70,15 +75,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I "/oauth2/token", // proxyPath false, // proxyToAdmin false, // camelToSnakeCaseConversion - () -> { - Map formFields = new HashMap<>(); - for (Map.Entry entry : bodyFromSDK.entrySet()) { - formFields.put(entry.getKey(), entry.getValue().getAsString()); - } - - return formFields; - }, - HashMap::new, + formFields, + new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { if (jsonBody == null) { throw new IllegalStateException("unexpected response from hydra"); @@ -88,8 +86,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(bodyFromSDK, "access_token", true); - JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(bodyFromSDK, "id_token", true); + JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "access_token", true); + JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "id_token", true); // useStaticKeyInput defaults to true, so we check if it has been explicitly set to false boolean useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java index 77a54f46b..60c9b80fa 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java @@ -56,24 +56,22 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I if (token.startsWith("st_rt_")) { String iss = InputParser.parseStringOrThrowError(input, "iss", false); + Map formFields = new HashMap<>(); + for (Map.Entry entry : input.entrySet()) { + formFields.put(entry.getKey(), entry.getValue().getAsString()); + } + try { OAuthProxyHelper.proxyFormPOST( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), - "/admin/oauth2/introspect", - true, - false, - () -> { - Map formFields = new HashMap<>(); - for (Map.Entry entry : input.entrySet()) { - formFields.put(entry.getKey(), entry.getValue().getAsString()); - } - - return formFields; - }, - HashMap::new, - (statusCode, headers, rawBody, jsonBody) -> { + "/admin/oauth2/introspect", // pathProxy + true, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFields, + new HashMap<>(), // getHeaders + (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject jsonObject = jsonBody.getAsJsonObject(); jsonObject.addProperty("iss", iss); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java index 9692574e2..42c2fcea9 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java @@ -58,8 +58,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I "/admin/clients/" + clientId, // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - () -> new JsonObject(), // getJsonBody - HashMap::new, // getHeadersForProxy + new JsonObject(), // getJsonBody + new HashMap<>(), // getHeadersForProxy (statusCode, headers, rawBody, jsonBody) -> { // handleResponse try { OAuth.removeClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); From b6ab81bf74360c8aec371c985fcbf1b46f993233 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 12 Sep 2024 12:36:09 +0530 Subject: [PATCH 27/40] fix: pr comments --- src/main/java/io/supertokens/oauth/OAuthToken.java | 8 -------- .../java/io/supertokens/oauth/Transformations.java | 12 +++--------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuthToken.java b/src/main/java/io/supertokens/oauth/OAuthToken.java index ec4cd5e21..0c838702b 100644 --- a/src/main/java/io/supertokens/oauth/OAuthToken.java +++ b/src/main/java/io/supertokens/oauth/OAuthToken.java @@ -100,14 +100,6 @@ public static String reSignToken(AppIdentifier appIdentifier, Main main, String JWTCreationException { JsonObject payload = JWT.getPayloadWithoutVerifying(token).payload; - // move keys in ext to root - if (tokenType == TokenType.ACCESS_TOKEN && payload.has("ext")) { - JsonObject ext = payload.getAsJsonObject("ext"); - for (Map.Entry entry : ext.entrySet()) { - payload.add(entry.getKey(), entry.getValue()); - } - payload.remove("ext"); - } payload.addProperty("iss", iss); payload.addProperty("stt", tokenType.getValue()); diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index f15d9166d..53a1df08c 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -50,19 +50,14 @@ private static String transformQueryParamsInURLFromHydra(String redirectTo) { for (String param : queryParams) { String[] keyValue = param.split("="); if (keyValue.length > 1 && keyValue[1].startsWith("ory_")) { - String decodedValue = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8.name()); - if (decodedValue.startsWith("ory_")) { - decodedValue = decodedValue.replaceFirst("ory_", "st_"); - } - String encodedValue = URLEncoder.encode(decodedValue, StandardCharsets.UTF_8.name()); - updatedQuery.append(keyValue[0]).append("=").append(encodedValue).append("&"); + updatedQuery.append(keyValue[0]).append("=").append(keyValue[1].replaceFirst("ory_", "st_")).append("&"); } else { updatedQuery.append(param).append("&"); } } redirectTo = url.getProtocol() + "://" + url.getHost() + ":" + url.getPort() + url.getPath() + "?" + updatedQuery.toString().trim(); - } catch (MalformedURLException | UnsupportedEncodingException e) { + } catch (MalformedURLException e) { throw new IllegalStateException(e); } @@ -177,10 +172,9 @@ public static Map> transformResponseHeadersFromHydra(Main m headers = new HashMap<>(headers); // make it modifyable - // Location transformation 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)); From d87769a8f767e73e87f55031ffdfa7b165792ae9 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 12 Sep 2024 12:40:49 +0530 Subject: [PATCH 28/40] fix: pr comments --- src/main/java/io/supertokens/oauth/Transformations.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index 53a1df08c..09ff8192d 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -1,10 +1,8 @@ package io.supertokens.oauth; -import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; -import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; @@ -70,9 +68,7 @@ public static List transformCookiesFromHydra(List cookies) { for (int i = 0; i < cookies.size(); i++) { String cookieStr = cookies.get(i); if (cookieStr.startsWith("ory_hydra_")) { - if (cookieStr.startsWith("ory_hydra_")) { - cookieStr = cookieStr.replaceFirst("ory_hydra_", "st_oauth_"); - } + cookieStr = cookieStr.replaceFirst("ory_hydra_", "st_oauth_"); cookies.set(i, cookieStr); } } From 085e5c18ec90b15d3b527510d1e459eb1e51d9a7 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 12 Sep 2024 14:03:56 +0530 Subject: [PATCH 29/40] fix: revert original http request --- .../supertokens/httpRequest/HttpRequest.java | 115 +++--------------- .../io/supertokens/test/HttpRequestTest.java | 84 +++---------- .../supertokens/test/JWKSPublicAPITest.java | 37 +++--- 3 files changed, 52 insertions(+), 184 deletions(-) diff --git a/src/main/java/io/supertokens/httpRequest/HttpRequest.java b/src/main/java/io/supertokens/httpRequest/HttpRequest.java index 73be6c072..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 { @@ -125,9 +130,8 @@ public static T sendGETRequest(Main main, String requestID, String url, Map< @SuppressWarnings("unchecked") public static T sendGETRequestWithResponseHeaders(Main main, String requestID, String url, Map params, - Map headers, int connectionTimeoutMS, int readTimeoutMS, Integer version, - Map> responseHeaders, boolean followRedirects) + Map responseHeaders) throws IOException, HttpResponseException { StringBuilder paramBuilder = new StringBuilder(); @@ -153,17 +157,12 @@ public static T sendGETRequestWithResponseHeaders(Main main, String requestI if (version != null) { con.setRequestProperty("api-version", version + ""); } - if (headers != null) { - for (Map.Entry entry : headers.entrySet()) { - con.setRequestProperty(entry.getKey(), entry.getValue()); - } - } - 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)); } }); @@ -268,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 { @@ -299,62 +274,4 @@ public static T sendJsonDELETERequest(Main main, String requestID, String ur "DELETE"); } - public static T sendFormPOSTRequest(Main main, String requestID, String url, Map formData, - int connectionTimeoutMS, int readTimeoutMS, Integer version) - throws IOException, HttpResponseException { - StringBuilder formDataBuilder = new StringBuilder(); - for (Map.Entry entry : formData.entrySet()) { - formDataBuilder.append(entry.getKey()).append("=") - .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)).append("&"); - } - String formDataStr = formDataBuilder.toString(); - if (!formDataStr.equals("")) { - formDataStr = formDataStr.substring(0, formDataStr.length() - 1); - } - - URL obj = getURL(main, requestID, url); - HttpURLConnection con = null; - try { - con = (HttpURLConnection) obj.openConnection(); - con.setRequestMethod("POST"); - con.setConnectTimeout(connectionTimeoutMS); - con.setReadTimeout(readTimeoutMS); - con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); - if (version != null) { - con.setRequestProperty("api-version", version + ""); - } - - con.setDoOutput(true); - try (OutputStream os = con.getOutputStream()) { - os.write(formDataStr.getBytes(StandardCharsets.UTF_8)); - } - - int responseCode = con.getResponseCode(); - InputStream inputStream = null; - if (responseCode < STATUS_CODE_ERROR_THRESHOLD) { - inputStream = con.getInputStream(); - } else { - inputStream = con.getErrorStream(); - } - - StringBuilder response = new StringBuilder(); - try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream))) { - String inputLine; - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - } - if (responseCode < STATUS_CODE_ERROR_THRESHOLD) { - if (!isJsonValid(response.toString())) { - return (T) response.toString(); - } - return (T) (new JsonParser().parse(response.toString())); - } - throw new HttpResponseException(responseCode, response.toString()); - } finally { - if (con != null) { - con.disconnect(); - } - } - } } \ No newline at end of file diff --git a/src/test/java/io/supertokens/test/HttpRequestTest.java b/src/test/java/io/supertokens/test/HttpRequestTest.java index eb0dac22b..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", new HashMap<>(), 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 9b13d6d2c..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, new HashMap<>(), - 1000, 1000, null, responseHeaders, true); + "http://localhost:3567/.well-known/jwks.json", null, + 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, new HashMap<>(), - 1000, 1000, null, responseHeaders, true); + "http://localhost:3567/.well-known/jwks.json", null, + 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(); From bdcdc295819d985b23266427cf762c778ec27c62 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 12 Sep 2024 14:32:22 +0530 Subject: [PATCH 30/40] fix: pr comment refactor --- src/main/java/io/supertokens/oauth/OAuth.java | 56 +++++++++-- .../CreateUpdateOrGetOAuthClientAPI.java | 13 ++- .../OAuthAcceptAuthConsentRequestAPI.java | 2 +- .../oauth/OAuthAcceptAuthLoginRequestAPI.java | 2 +- .../OAuthAcceptAuthLogoutRequestAPI.java | 2 +- .../webserver/api/oauth/OAuthAuthAPI.java | 2 +- .../api/oauth/OAuthClientListAPI.java | 2 +- .../oauth/OAuthGetAuthConsentRequestAPI.java | 2 +- .../oauth/OAuthGetAuthLoginRequestAPI.java | 2 +- .../oauth/OAuthGetAuthLogoutRequestAPI.java | 2 +- .../webserver/api/oauth/OAuthProxyHelper.java | 92 +++++++------------ .../OAuthRejectAuthConsentRequestAPI.java | 2 +- .../oauth/OAuthRejectAuthLoginRequestAPI.java | 2 +- .../OAuthRejectAuthLogoutRequestAPI.java | 2 +- .../webserver/api/oauth/OAuthTokenAPI.java | 2 +- .../api/oauth/OAuthTokenIntrospectAPI.java | 16 ++-- .../api/oauth/RemoveOAuthClientAPI.java | 6 +- 17 files changed, 110 insertions(+), 97 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 18b9e8fea..36968478f 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -63,10 +63,14 @@ private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) "feature."); } - public static HttpRequest.Response handleOAuthProxyGET(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, Map queryParams, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequest.Response doOAuthProxyGET(Main main, AppIdentifier appIdentifier, Storage storage, 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 (queryParams != null && queryParams.containsKey("client_id")) { String clientId = queryParams.get("client_id"); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { @@ -94,13 +98,22 @@ public static HttpRequest.Response handleOAuthProxyGET(Main main, AppIdentifier checkNonSuccessResponse(response); + if (camelToSnakeCaseConversion) { + response.jsonResponse = convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + + return response; } - public static HttpRequest.Response handleOAuthProxyFormPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, Map formFields, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequest.Response doOAuthProxyFormPOST(Main main, AppIdentifier appIdentifier, Storage storage, 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); + if (camelToSnakeCaseConversion) { + formFields = OAuth.convertCamelToSnakeCase(formFields); + } + if (formFields.containsKey("client_id")) { String clientId = formFields.get("client_id"); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { @@ -128,13 +141,21 @@ public static HttpRequest.Response handleOAuthProxyFormPOST(Main main, AppIdenti checkNonSuccessResponse(response); + if (camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + return response; } - public static HttpRequest.Response handleOAuthProxyJsonPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequest.Response doOAuthProxyJsonPOST(Main main, AppIdentifier appIdentifier, Storage storage, 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); + if (camelToSnakeCaseConversion) { + jsonInput = convertCamelToSnakeCase(jsonInput); + } + if (jsonInput.has("client_id")) { String clientId = jsonInput.get("client_id").getAsString(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { @@ -162,13 +183,22 @@ public static HttpRequest.Response handleOAuthProxyJsonPOST(Main main, AppIdenti checkNonSuccessResponse(response); + if (camelToSnakeCaseConversion) { + response.jsonResponse = convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + return response; } - public static HttpRequest.Response handleOAuthProxyJsonPUT(Main main, AppIdentifier appIdentifier, Storage storage, String path, Map queryParams, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequest.Response doOAuthProxyJsonPUT(Main main, AppIdentifier appIdentifier, Storage storage, 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); + if (camelToSnakeCaseConversion) { + queryParams = convertCamelToSnakeCase(queryParams); + jsonInput = convertCamelToSnakeCase(jsonInput); + } + if (jsonInput.has("client_id")) { String clientId = jsonInput.get("client_id").getAsString(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { @@ -196,13 +226,21 @@ public static HttpRequest.Response handleOAuthProxyJsonPUT(Main main, AppIdentif checkNonSuccessResponse(response); + if (camelToSnakeCaseConversion) { + response.jsonResponse = convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + return response; } - public static HttpRequest.Response handleOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequest.Response doOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, 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); + if (camelToSnakeCaseConversion) { + jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); + } + if (jsonInput.has("client_id")) { String clientId = jsonInput.get("client_id").getAsString(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { @@ -230,6 +268,10 @@ public static HttpRequest.Response handleOAuthProxyJsonDELETE(Main main, AppIden checkNonSuccessResponse(response); + if (camelToSnakeCaseConversion) { + response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); + } + return response; } @@ -284,7 +326,7 @@ public static List listClientIds(Main main, AppIdentifier appIdentifier, return oauthStorage.listClientsForApp(appIdentifier); } - public static Map convertCamelToSnakeCase(Map queryParams) { + private static Map convertCamelToSnakeCase(Map queryParams) { Map result = new HashMap<>(); if (queryParams != null) { for (Map.Entry entry : queryParams.entrySet()) { @@ -302,7 +344,7 @@ public static JsonObject convertCamelToSnakeCase(JsonObject queryParams) { return result; } - public static JsonElement convertSnakeCaseToCamelCaseRecursively(JsonElement jsonResponse) { + private static JsonElement convertSnakeCaseToCamelCaseRecursively(JsonElement jsonResponse) { if (jsonResponse == null) { return null; } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index 60fc8f956..d3b9e8d57 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -66,7 +66,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), new HashMap<>(), // getHeadersForProxy (statusCode, headers, rawBody, jsonBody) -> { // handleResponse - this.sendJsonResponse(200, jsonBody, resp); + return jsonBody.getAsJsonObject(); } ); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { @@ -103,7 +103,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } catch (OAuth2ClientAlreadyExistsForAppException e) { // ignore } - this.sendJsonResponse(200, jsonBody, resp); + return jsonBody.getAsJsonObject(); } ); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { @@ -119,16 +119,15 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO // Apply existing client config on top of input try { Map queryParams = new HashMap<>(); - queryParams.put("client_id", clientId); - HttpRequest.Response response = OAuth.handleOAuthProxyGET( + queryParams.put("clientId", clientId); + HttpRequest.Response response = OAuth.doOAuthProxyGET( main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), "/admin/clients/" + clientId, - true, queryParams, null); + true, true, queryParams, null); JsonObject existingConfig = response.jsonResponse.getAsJsonObject(); - existingConfig = OAuth.convertSnakeCaseToCamelCaseRecursively(existingConfig).getAsJsonObject(); for (Map.Entry entry : existingConfig.entrySet()) { String key = entry.getKey(); if (!input.has(key)) { @@ -153,7 +152,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO input, // jsonBody new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { // handleResponse - this.sendJsonResponse(200, jsonBody, resp); + return jsonBody.getAsJsonObject(); } ); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java index 9532543ae..b4e9447ab 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java @@ -44,7 +44,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + return response; } ); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException 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 index 3dfd9a07a..6ba1797ae 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java @@ -44,7 +44,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO (statusCode, headers, rawBody, jsonBody) -> { JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + return response; } ); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException 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 index 0a93ad6c7..0f520184d 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java @@ -44,7 +44,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO (statusCode, headers, rawBody, jsonBody) -> { JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + return response; } ); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException 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 0eb2df1e5..944f37600 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -93,7 +93,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I response.add("cookies", jsonCookies); response.addProperty("status", "OK"); - super.sendJsonResponse(200, response, resp); + return response; } ); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java index d9fc6a46f..96bc6a5d6 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java @@ -67,7 +67,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO } response.add("clients", clients); - sendJsonResponse(200, response, resp); + return response; } ); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java index 0c1b7f632..e7432dbe9 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java @@ -40,7 +40,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + return response; } ); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java index 9cadc4708..4d3546720 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java @@ -40,7 +40,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + return response; } ); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java index 0349bbce4..308c3164f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java @@ -40,7 +40,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + return response; } ); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java index dff334f77..58855afa4 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java @@ -33,24 +33,19 @@ public class OAuthProxyHelper { public static void proxyGET(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, Map headers, - HandleResponse handleResponse) throws IOException, ServletException { - if (camelToSnakeCaseConversion) { - queryParams = OAuth.convertCamelToSnakeCase(queryParams); - } - + GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequest.Response response = OAuth.handleOAuthProxyGET(main, appIdentifier, storage, path, proxyToAdmin, queryParams, headers); - - if (camelToSnakeCaseConversion) { - response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); - } + HttpRequest.Response response = OAuth.doOAuthProxyGET(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, headers); - handleResponse.apply( + JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, response.headers, response.rawResponse, response.jsonResponse ); + resp.setStatus(200); + resp.setHeader("Content-Type", "application/json; charset=UTF-8"); + resp.getWriter().println(jsonResponse.toString()); } catch (OAuthClientNotFoundException e) { handleOAuthClientNotFoundException(resp); @@ -64,24 +59,19 @@ public static void proxyGET(Main main, HttpServletRequest req, HttpServletRespon public static void proxyFormPOST(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map formFields, Map headers, - HandleResponse handleResponse) throws IOException, ServletException { - if (camelToSnakeCaseConversion) { - formFields = OAuth.convertCamelToSnakeCase(formFields); - } - + GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequest.Response response = OAuth.handleOAuthProxyFormPOST(main, appIdentifier, storage, path, proxyToAdmin, formFields, headers); + HttpRequest.Response response = OAuth.doOAuthProxyFormPOST(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, formFields, headers); - if (camelToSnakeCaseConversion) { - response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); - } - - handleResponse.apply( + JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, response.headers, response.rawResponse, response.jsonResponse ); + resp.setStatus(200); + resp.setHeader("Content-Type", "application/json; charset=UTF-8"); + resp.getWriter().println(jsonResponse.toString()); } catch (OAuthClientNotFoundException e) { handleOAuthClientNotFoundException(resp); @@ -95,24 +85,19 @@ public static void proxyFormPOST(Main main, HttpServletRequest req, HttpServletR public static void proxyJsonPOST(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, JsonObject jsonInput, Map headers, - HandleResponse handleResponse) throws IOException, ServletException { - if (camelToSnakeCaseConversion) { - jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); - } - + GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequest.Response response = OAuth.handleOAuthProxyJsonPOST(main, appIdentifier, storage, path, proxyToAdmin, jsonInput, headers); + HttpRequest.Response response = OAuth.doOAuthProxyJsonPOST(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); - if (camelToSnakeCaseConversion) { - response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); - } - - handleResponse.apply( + JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, response.headers, response.rawResponse, response.jsonResponse ); + resp.setStatus(200); + resp.setHeader("Content-Type", "application/json; charset=UTF-8"); + resp.getWriter().println(jsonResponse.toString()); } catch (OAuthClientNotFoundException e) { handleOAuthClientNotFoundException(resp); @@ -126,28 +111,20 @@ public static void proxyJsonPOST(Main main, HttpServletRequest req, HttpServletR public static void proxyJsonPUT(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, JsonObject jsonInput, - Map headers, HandleResponse handleResponse) throws IOException, ServletException { - if (camelToSnakeCaseConversion) { - queryParams = OAuth.convertCamelToSnakeCase(queryParams); - } - - if (camelToSnakeCaseConversion) { - jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); - } + Map headers, GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequest.Response response = OAuth.handleOAuthProxyJsonPUT(main, appIdentifier, storage, path, queryParams, proxyToAdmin, jsonInput, headers); - - if (camelToSnakeCaseConversion) { - response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); - } + HttpRequest.Response response = OAuth.doOAuthProxyJsonPUT(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); - handleResponse.apply( + JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, response.headers, response.rawResponse, response.jsonResponse ); + resp.setStatus(200); + resp.setHeader("Content-Type", "application/json; charset=UTF-8"); + resp.getWriter().println(jsonResponse.toString()); } catch (OAuthClientNotFoundException e) { handleOAuthClientNotFoundException(resp); @@ -161,24 +138,19 @@ public static void proxyJsonPUT(Main main, HttpServletRequest req, HttpServletRe public static void proxyJsonDELETE(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, JsonObject jsonInput, Map headers, - HandleResponse handleResponse) throws IOException, ServletException { - if (camelToSnakeCaseConversion) { - jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); - } - + GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequest.Response response = OAuth.handleOAuthProxyJsonDELETE(main, appIdentifier, storage, path, proxyToAdmin, jsonInput, headers); - - if (camelToSnakeCaseConversion) { - response.jsonResponse = OAuth.convertSnakeCaseToCamelCaseRecursively(response.jsonResponse); - } + HttpRequest.Response response = OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); - handleResponse.apply( + JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, response.headers, response.rawResponse, response.jsonResponse ); + resp.setStatus(200); + resp.setHeader("Content-Type", "application/json; charset=UTF-8"); + resp.getWriter().println(jsonResponse.toString()); } catch (OAuthClientNotFoundException e) { handleOAuthClientNotFoundException(resp); @@ -207,8 +179,8 @@ public static Map defaultGetQueryParamsFromRequest(HttpServletRe } @FunctionalInterface - public interface HandleResponse { - void apply(int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException; + public interface GetJsonResponse { + JsonObject apply(int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException; } private static void handleOAuthClientNotFoundException(HttpServletResponse resp) throws IOException { diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java index 816229714..c90639573 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java @@ -44,7 +44,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + return response; } ); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException 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 index aff51a895..c6cd0f9e5 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java @@ -45,7 +45,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + return response; } ); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException 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 41311b9d4..a43309e62 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java @@ -44,7 +44,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO (statusCode, headers, rawBody, jsonBody) -> { // handleResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); - sendJsonResponse(200, response, resp); + return response; } ); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 23ee2ead5..8389b0432 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -98,7 +98,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } jsonBody.getAsJsonObject().addProperty("status", "OK"); - super.sendJsonResponse(200, jsonBody, resp); + return jsonBody.getAsJsonObject(); } ); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java index 60c9b80fa..607e5780f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java @@ -72,19 +72,19 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I formFields, new HashMap<>(), // getHeaders (statusCode, headers, rawBody, jsonBody) -> { // handleResponse - JsonObject jsonObject = jsonBody.getAsJsonObject(); + JsonObject response = jsonBody.getAsJsonObject(); - jsonObject.addProperty("iss", iss); - if (jsonObject.has("ext")) { - JsonObject ext = jsonObject.get("ext").getAsJsonObject(); + response.addProperty("iss", iss); + if (response.has("ext")) { + JsonObject ext = response.get("ext").getAsJsonObject(); for (Map.Entry entry : ext.entrySet()) { - jsonObject.add(entry.getKey(), entry.getValue()); + response.add(entry.getKey(), entry.getValue()); } - jsonObject.remove("ext"); + response.remove("ext"); } - jsonObject.addProperty("status", "OK"); - super.sendJsonResponse(200, jsonBody, resp); + response.addProperty("status", "OK"); + return response; } ); } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java index 42c2fcea9..2a58a4427 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java @@ -67,9 +67,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I throw new ServletException(e); } - JsonObject responseBody = new JsonObject(); - responseBody.addProperty("status", "OK"); - this.sendJsonResponse(200, responseBody, resp); + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + return response; } ); From d8a1b874ca6066542a05a4b995bd3e2874f1fc7e Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 16 Sep 2024 08:28:41 +0530 Subject: [PATCH 31/40] fix: pr comment --- src/main/java/io/supertokens/oauth/OAuthToken.java | 13 +++++++------ .../webserver/api/oauth/OAuthTokenAPI.java | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuthToken.java b/src/main/java/io/supertokens/oauth/OAuthToken.java index 0c838702b..d2433a1a3 100644 --- a/src/main/java/io/supertokens/oauth/OAuthToken.java +++ b/src/main/java/io/supertokens/oauth/OAuthToken.java @@ -49,9 +49,8 @@ public int getValue() { private static Set NON_OVERRIDABLE_TOKEN_PROPS = Set.of( "kid", "typ", "alg", "aud", "iss", "iat", "exp", "nbf", "jti", "ext", - "sid", "rat", "at_hash", - "client_id", "scp", "sub", "rsub", - "sessionHandle", "tId", "stt" + "sid", "rat", "at_hash", "rt_hash", + "client_id", "scp", "sub", "stt" ); public static JsonObject getPayloadFromJWTToken(AppIdentifier appIdentifier, @@ -103,9 +102,11 @@ public static String reSignToken(AppIdentifier appIdentifier, Main main, String payload.addProperty("iss", iss); payload.addProperty("stt", tokenType.getValue()); - for (Map.Entry entry : payloadUpdate.entrySet()) { - if (!NON_OVERRIDABLE_TOKEN_PROPS.contains(entry.getKey())) { - payload.add(entry.getKey(), entry.getValue()); + if (payloadUpdate != null) { + for (Map.Entry entry : payloadUpdate.entrySet()) { + if (!NON_OVERRIDABLE_TOKEN_PROPS.contains(entry.getKey())) { + payload.add(entry.getKey(), entry.getValue()); + } } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 8389b0432..4567e7f61 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -60,7 +60,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String iss = InputParser.parseStringOrThrowError(input, "iss", false); // input validation JsonObject bodyFromSDK = InputParser.parseJsonObjectOrThrowError(input, "inputBody", false); + // 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); Map formFields = new HashMap<>(); for (Map.Entry entry : bodyFromSDK.entrySet()) { @@ -89,8 +91,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "access_token", true); JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "id_token", true); - // useStaticKeyInput defaults to true, so we check if it has been explicitly set to false - boolean useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); jsonBody = OAuth.transformTokens(super.main, appIdentifier, storage, jsonBody.getAsJsonObject(), iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { From 360b266a8d04e43826b16926e916271bfb947071 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 16 Sep 2024 09:07:13 +0530 Subject: [PATCH 32/40] fix: pr comment --- devConfig.yaml | 2 +- src/main/java/io/supertokens/config/CoreConfig.java | 2 +- src/main/java/io/supertokens/oauth/Transformations.java | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/devConfig.yaml b/devConfig.yaml index f55e424fe..ed83e6bb5 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -166,4 +166,4 @@ 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: http://localhost:4001 +# oauth_provider_url_configured_in_hydra: diff --git a/src/main/java/io/supertokens/config/CoreConfig.java b/src/main/java/io/supertokens/config/CoreConfig.java index 036aebaf6..b82053248 100644 --- a/src/main/java/io/supertokens/config/CoreConfig.java +++ b/src/main/java/io/supertokens/config/CoreConfig.java @@ -297,7 +297,7 @@ 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'") + "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 diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index 09ff8192d..77b5940c4 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -120,7 +120,7 @@ private static String transformRedirectUrlFromHydra(Main main, AppIdentifier app String hydraBaseUrlForConsentAndLogin = Config .getConfig(appIdentifier.getAsPublicTenantIdentifier(), main) .getOauthProviderConsentLoginBaseUrl(); - + if (!redirectTo.startsWith("/")) { redirectTo = transformQueryParamsInURLFromHydra(redirectTo); @@ -143,6 +143,8 @@ private static String transformRedirectUrlFromHydra(Main main, AppIdentifier app 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) { From f3c022f0052f19bc5c4eed687eeeb189beafe424 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 18 Sep 2024 13:14:07 +0530 Subject: [PATCH 33/40] fix: pr comment --- .../io/supertokens/webserver/api/oauth/OAuthTokenAPI.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 4567e7f61..5c64dd8b4 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -60,6 +60,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String iss = InputParser.parseStringOrThrowError(input, "iss", false); // input validation JsonObject bodyFromSDK = InputParser.parseJsonObjectOrThrowError(input, "inputBody", false); + JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "access_token", true); + JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "id_token", true); + // 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); @@ -88,9 +91,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I AppIdentifier appIdentifier = getAppIdentifier(req); Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "access_token", true); - JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "id_token", true); - jsonBody = OAuth.transformTokens(super.main, appIdentifier, storage, jsonBody.getAsJsonObject(), iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { From 095a27fcdbaf5655065a49ed29e546461d57806c Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 18 Sep 2024 13:21:07 +0530 Subject: [PATCH 34/40] fix: pr comments --- ...ttpRequest.java => HttpRequestForOry.java} | 8 ++++++- src/main/java/io/supertokens/oauth/OAuth.java | 22 +++++++++---------- .../CreateUpdateOrGetOAuthClientAPI.java | 10 ++++----- .../OAuthAcceptAuthConsentRequestAPI.java | 2 +- .../webserver/api/oauth/OAuthAuthAPI.java | 2 +- .../api/oauth/OAuthClientListAPI.java | 2 +- .../oauth/OAuthGetAuthConsentRequestAPI.java | 2 +- .../oauth/OAuthGetAuthLoginRequestAPI.java | 2 +- .../oauth/OAuthGetAuthLogoutRequestAPI.java | 2 +- .../webserver/api/oauth/OAuthProxyHelper.java | 12 +++++----- .../OAuthRejectAuthConsentRequestAPI.java | 2 +- .../oauth/OAuthRejectAuthLoginRequestAPI.java | 2 +- .../OAuthRejectAuthLogoutRequestAPI.java | 2 +- .../api/oauth/OAuthTokenIntrospectAPI.java | 2 +- .../api/oauth/RemoveOAuthClientAPI.java | 2 +- 15 files changed, 40 insertions(+), 34 deletions(-) rename src/main/java/io/supertokens/oauth/{HttpRequest.java => HttpRequestForOry.java} (94%) diff --git a/src/main/java/io/supertokens/oauth/HttpRequest.java b/src/main/java/io/supertokens/oauth/HttpRequestForOry.java similarity index 94% rename from src/main/java/io/supertokens/oauth/HttpRequest.java rename to src/main/java/io/supertokens/oauth/HttpRequestForOry.java index ff3b954d7..62f5945e8 100644 --- a/src/main/java/io/supertokens/oauth/HttpRequest.java +++ b/src/main/java/io/supertokens/oauth/HttpRequestForOry.java @@ -15,7 +15,13 @@ import java.util.Map; import java.util.stream.Collectors; -public class HttpRequest { +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; diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 36968478f..7e60dcc70 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -63,7 +63,7 @@ private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) "feature."); } - public static HttpRequest.Response doOAuthProxyGET(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequestForOry.Response doOAuthProxyGET(Main main, AppIdentifier appIdentifier, Storage storage, 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); @@ -90,7 +90,7 @@ public static HttpRequest.Response doOAuthProxyGET(Main main, AppIdentifier appI } String fullUrl = baseURL + path; - HttpRequest.Response response = HttpRequest.doGet(fullUrl, headers, queryParams); + HttpRequestForOry.Response response = HttpRequestForOry.doGet(fullUrl, headers, queryParams); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); @@ -106,7 +106,7 @@ public static HttpRequest.Response doOAuthProxyGET(Main main, AppIdentifier appI return response; } - public static HttpRequest.Response doOAuthProxyFormPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map formFields, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequestForOry.Response doOAuthProxyFormPOST(Main main, AppIdentifier appIdentifier, Storage storage, 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); @@ -133,7 +133,7 @@ public static HttpRequest.Response doOAuthProxyFormPOST(Main main, AppIdentifier } String fullUrl = baseURL + path; - HttpRequest.Response response = HttpRequest.doFormPost(fullUrl, headers, formFields); + HttpRequestForOry.Response response = HttpRequestForOry.doFormPost(fullUrl, headers, formFields); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); @@ -148,7 +148,7 @@ public static HttpRequest.Response doOAuthProxyFormPOST(Main main, AppIdentifier return response; } - public static HttpRequest.Response doOAuthProxyJsonPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequestForOry.Response doOAuthProxyJsonPOST(Main main, AppIdentifier appIdentifier, Storage storage, 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); @@ -175,7 +175,7 @@ public static HttpRequest.Response doOAuthProxyJsonPOST(Main main, AppIdentifier } String fullUrl = baseURL + path; - HttpRequest.Response response = HttpRequest.doJsonPost(fullUrl, headers, jsonInput); + HttpRequestForOry.Response response = HttpRequestForOry.doJsonPost(fullUrl, headers, jsonInput); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); @@ -190,7 +190,7 @@ public static HttpRequest.Response doOAuthProxyJsonPOST(Main main, AppIdentifier return response; } - public static HttpRequest.Response doOAuthProxyJsonPUT(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequestForOry.Response doOAuthProxyJsonPUT(Main main, AppIdentifier appIdentifier, Storage storage, 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); @@ -218,7 +218,7 @@ public static HttpRequest.Response doOAuthProxyJsonPUT(Main main, AppIdentifier } String fullUrl = baseURL + path; - HttpRequest.Response response = HttpRequest.doJsonPut(fullUrl, queryParams, headers, jsonInput); + HttpRequestForOry.Response response = HttpRequestForOry.doJsonPut(fullUrl, queryParams, headers, jsonInput); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); @@ -233,7 +233,7 @@ public static HttpRequest.Response doOAuthProxyJsonPUT(Main main, AppIdentifier return response; } - public static HttpRequest.Response doOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String 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); @@ -260,7 +260,7 @@ public static HttpRequest.Response doOAuthProxyJsonDELETE(Main main, AppIdentifi } String fullUrl = baseURL + path; - HttpRequest.Response response = HttpRequest.doJsonDelete(fullUrl, headers, jsonInput); + HttpRequestForOry.Response response = HttpRequestForOry.doJsonDelete(fullUrl, headers, jsonInput); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); @@ -275,7 +275,7 @@ public static HttpRequest.Response doOAuthProxyJsonDELETE(Main main, AppIdentifi return response; } - private static void checkNonSuccessResponse(HttpRequest.Response response) throws OAuthAPIException, OAuthClientNotFoundException { + private static void checkNonSuccessResponse(HttpRequestForOry.Response response) throws OAuthAPIException, OAuthClientNotFoundException { if (response.statusCode == 404) { throw new OAuthClientNotFoundException(); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index d3b9e8d57..108cba0ca 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -26,7 +26,7 @@ import io.supertokens.Main; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.oauth.HttpRequest; +import io.supertokens.oauth.HttpRequestForOry; import io.supertokens.oauth.OAuth; import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.oauth.exceptions.OAuthClientNotFoundException; @@ -65,7 +65,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO true, // camelToSnakeCaseConversion OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), new HashMap<>(), // getHeadersForProxy - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse return jsonBody.getAsJsonObject(); } ); @@ -93,7 +93,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I true, // camelToSnakeCaseConversion input, // jsonBody new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse String clientId = jsonBody.getAsJsonObject().get("clientId").getAsString(); try { @@ -120,7 +120,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO try { Map queryParams = new HashMap<>(); queryParams.put("clientId", clientId); - HttpRequest.Response response = OAuth.doOAuthProxyGET( + HttpRequestForOry.Response response = OAuth.doOAuthProxyGET( main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -151,7 +151,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO new HashMap<>(), // queryParams input, // jsonBody new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse return jsonBody.getAsJsonObject(); } ); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java index b4e9447ab..2e3f814dc 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java @@ -41,7 +41,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), input, // jsonBody new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); return response; 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 944f37600..a8752f6bf 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -73,7 +73,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I false, // camelToSnakeCaseConversion queryParams, headers, - (statusCode, responseHeaders, rawBody, jsonBody) -> { // handleResponse + (statusCode, responseHeaders, rawBody, jsonBody) -> { // getJsonResponse if (headers == null || !responseHeaders.containsKey("Location")) { throw new IllegalStateException("Invalid response from hydra"); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java index 96bc6a5d6..d177c7fcf 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java @@ -44,7 +44,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO true, // camelToSnakeCaseConversion new HashMap<>(), // queryParams new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse JsonObject response = new JsonObject(); response.addProperty("status", "OK"); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java index e7432dbe9..4faa5e65f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java @@ -37,7 +37,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO true, // camelToSnakeCaseConversion OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); return response; diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java index 4d3546720..ca9a4af6a 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java @@ -37,7 +37,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO true, // camelToSnakeCaseConversion OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); return response; diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java index 308c3164f..0b4f9433f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java @@ -37,7 +37,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO true, // camelToSnakeCaseConversion OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); return response; diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java index 58855afa4..a4acb0593 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java @@ -13,7 +13,7 @@ import io.supertokens.Main; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; -import io.supertokens.oauth.HttpRequest; +import io.supertokens.oauth.HttpRequestForOry; import io.supertokens.oauth.OAuth; import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.oauth.exceptions.OAuthClientNotFoundException; @@ -35,7 +35,7 @@ public static void proxyGET(Main main, HttpServletRequest req, HttpServletRespon Map queryParams, Map headers, GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequest.Response response = OAuth.doOAuthProxyGET(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, headers); + HttpRequestForOry.Response response = OAuth.doOAuthProxyGET(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, headers); JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, @@ -61,7 +61,7 @@ public static void proxyFormPOST(Main main, HttpServletRequest req, HttpServletR Map formFields, Map headers, GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequest.Response response = OAuth.doOAuthProxyFormPOST(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, formFields, headers); + HttpRequestForOry.Response response = OAuth.doOAuthProxyFormPOST(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, formFields, headers); JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, @@ -87,7 +87,7 @@ public static void proxyJsonPOST(Main main, HttpServletRequest req, HttpServletR JsonObject jsonInput, Map headers, GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequest.Response response = OAuth.doOAuthProxyJsonPOST(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); + HttpRequestForOry.Response response = OAuth.doOAuthProxyJsonPOST(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, @@ -114,7 +114,7 @@ public static void proxyJsonPUT(Main main, HttpServletRequest req, HttpServletRe Map headers, GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequest.Response response = OAuth.doOAuthProxyJsonPUT(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); + HttpRequestForOry.Response response = OAuth.doOAuthProxyJsonPUT(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, @@ -140,7 +140,7 @@ public static void proxyJsonDELETE(Main main, HttpServletRequest req, HttpServle JsonObject jsonInput, Map headers, GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequest.Response response = OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); + HttpRequestForOry.Response response = OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java index c90639573..cc886d09d 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java @@ -41,7 +41,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), input, // getJsonBody new HashMap<>(), // getHeadersForProxy - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); return response; diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java index c6cd0f9e5..ea676b754 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java @@ -42,7 +42,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), input, // jsonBody new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); return response; 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 a43309e62..dca66535f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java @@ -41,7 +41,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), input, // jsonBody new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("status", "OK"); return response; diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java index 607e5780f..101f56e09 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java @@ -71,7 +71,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I false, // camelToSnakeCaseConversion formFields, new HashMap<>(), // getHeaders - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("iss", iss); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java index 2a58a4427..1063c667f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java @@ -60,7 +60,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I true, // camelToSnakeCaseConversion new JsonObject(), // getJsonBody new HashMap<>(), // getHeadersForProxy - (statusCode, headers, rawBody, jsonBody) -> { // handleResponse + (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse try { OAuth.removeClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { From da96bc4e4139f38ce297344207ff610e01c79361 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 18 Sep 2024 14:00:40 +0530 Subject: [PATCH 35/40] fix: owner and pagination --- .../CreateUpdateOrGetOAuthClientAPI.java | 11 +++++- .../api/oauth/OAuthClientListAPI.java | 37 +++++++++++++++++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index 108cba0ca..11156c392 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -31,8 +31,10 @@ 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.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.webserver.InputParser; @@ -84,10 +86,15 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I input.addProperty("subjectType", "public"); try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + input.addProperty("owner", appIdentifier.getAppId()); + OAuthProxyHelper.proxyJsonPOST( main, req, resp, - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), + appIdentifier, + storage, "/admin/clients", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java index d177c7fcf..a79ff9e92 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java @@ -4,6 +4,7 @@ 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; @@ -14,7 +15,9 @@ 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.WebserverAPI; import jakarta.servlet.ServletException; @@ -35,14 +38,19 @@ public String getPath() { @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()); + OAuthProxyHelper.proxyGET( main, req, resp, - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), + appIdentifier, + storage, "/admin/clients", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - new HashMap<>(), // queryParams + queryParams, new HashMap<>(), // headers (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse JsonObject response = new JsonObject(); @@ -67,6 +75,29 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO } response.add("clients", clients); + + // pagination + List linkHeader = 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) { + response.addProperty("nextPaginationToken", pageToken); + } + } + } + return response; } ); From 160dce50a244139c0a1c581c09932aaab22b2b7c Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 18 Sep 2024 14:04:47 +0530 Subject: [PATCH 36/40] fix: pr comment --- src/main/java/io/supertokens/oauth/OAuth.java | 1 - src/main/java/io/supertokens/oauth/Transformations.java | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 7e60dcc70..ba5d61120 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -79,7 +79,6 @@ public static HttpRequestForOry.Response doOAuthProxyGET(Main main, AppIdentifie } // Request transformations - queryParams = Transformations.transformQueryParamsForHydra(queryParams); headers = Transformations.transformRequestHeadersForHydra(headers); String baseURL; diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index 77b5940c4..45df22a7a 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -22,10 +22,6 @@ public class Transformations { - public static Map transformQueryParamsForHydra(Map queryParams) { - return queryParams; - } - public static Map transformRequestHeadersForHydra(Map requestHeaders) { if (requestHeaders == null) { return requestHeaders; From bd0a91871b2e009547bf5c15646f50636925ad9a Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 18 Sep 2024 14:19:42 +0530 Subject: [PATCH 37/40] fix: client id check --- src/main/java/io/supertokens/oauth/OAuth.java | 35 ++++++++----------- .../CreateUpdateOrGetOAuthClientAPI.java | 4 +++ .../OAuthAcceptAuthConsentRequestAPI.java | 1 + .../oauth/OAuthAcceptAuthLoginRequestAPI.java | 1 + .../OAuthAcceptAuthLogoutRequestAPI.java | 1 + .../webserver/api/oauth/OAuthAuthAPI.java | 1 + .../api/oauth/OAuthClientListAPI.java | 1 + .../oauth/OAuthGetAuthConsentRequestAPI.java | 1 + .../oauth/OAuthGetAuthLoginRequestAPI.java | 1 + .../oauth/OAuthGetAuthLogoutRequestAPI.java | 1 + .../webserver/api/oauth/OAuthProxyHelper.java | 20 +++++------ .../OAuthRejectAuthConsentRequestAPI.java | 1 + .../oauth/OAuthRejectAuthLoginRequestAPI.java | 1 + .../OAuthRejectAuthLogoutRequestAPI.java | 1 + .../webserver/api/oauth/OAuthTokenAPI.java | 1 + .../api/oauth/OAuthTokenIntrospectAPI.java | 1 + .../api/oauth/RemoveOAuthClientAPI.java | 1 + 17 files changed, 43 insertions(+), 30 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index ba5d61120..ba323cca2 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -63,7 +63,7 @@ private static void checkForOauthFeature(AppIdentifier appIdentifier, Main main) "feature."); } - public static HttpRequestForOry.Response doOAuthProxyGET(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + 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); @@ -71,9 +71,8 @@ public static HttpRequestForOry.Response doOAuthProxyGET(Main main, AppIdentifie queryParams = convertCamelToSnakeCase(queryParams); } - if (queryParams != null && queryParams.containsKey("client_id")) { - String clientId = queryParams.get("client_id"); - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { + if (clientIdToCheck != null) { + if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -105,7 +104,7 @@ public static HttpRequestForOry.Response doOAuthProxyGET(Main main, AppIdentifie return response; } - public static HttpRequestForOry.Response doOAuthProxyFormPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map formFields, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + 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); @@ -113,9 +112,8 @@ public static HttpRequestForOry.Response doOAuthProxyFormPOST(Main main, AppIden formFields = OAuth.convertCamelToSnakeCase(formFields); } - if (formFields.containsKey("client_id")) { - String clientId = formFields.get("client_id"); - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { + if (clientIdToCheck != null) { + if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -147,7 +145,7 @@ public static HttpRequestForOry.Response doOAuthProxyFormPOST(Main main, AppIden return response; } - public static HttpRequestForOry.Response doOAuthProxyJsonPOST(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + 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); @@ -155,9 +153,8 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPOST(Main main, AppIden jsonInput = convertCamelToSnakeCase(jsonInput); } - if (jsonInput.has("client_id")) { - String clientId = jsonInput.get("client_id").getAsString(); - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { + if (clientIdToCheck != null) { + if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -189,7 +186,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPOST(Main main, AppIden return response; } - public static HttpRequestForOry.Response doOAuthProxyJsonPUT(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + 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); @@ -198,9 +195,8 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPUT(Main main, AppIdent jsonInput = convertCamelToSnakeCase(jsonInput); } - if (jsonInput.has("client_id")) { - String clientId = jsonInput.get("client_id").getAsString(); - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { + if (clientIdToCheck != null) { + if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -232,7 +228,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPUT(Main main, AppIdent return response; } - public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -240,9 +236,8 @@ public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppId jsonInput = OAuth.convertCamelToSnakeCase(jsonInput); } - if (jsonInput.has("client_id")) { - String clientId = jsonInput.get("client_id").getAsString(); - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { + if (clientIdToCheck != null) { + if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index 11156c392..3e314fa75 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -62,6 +62,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + clientId, // clientIdToCheck "/admin/clients/" + clientId, // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion @@ -95,6 +96,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I main, req, resp, appIdentifier, storage, + null, // clientIdToCheck "/admin/clients", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion @@ -131,6 +133,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + clientId, "/admin/clients/" + clientId, true, true, queryParams, null); @@ -152,6 +155,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + clientId, // clientIdToCheck "/admin/clients/" + clientId, true, // proxyToAdmin true, // camelToSnakeCaseConversion diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java index 2e3f814dc..252af8542 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java @@ -35,6 +35,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck "/admin/oauth2/auth/requests/consent/accept", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java index 6ba1797ae..ea73b7bf4 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java @@ -35,6 +35,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck "/admin/oauth2/auth/requests/login/accept", true, true, 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 0f520184d..c321b50bf 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java @@ -35,6 +35,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck "/admin/oauth2/auth/requests/logout/accept", true, true, 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 a8752f6bf..0662d0405 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -68,6 +68,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + queryParams.get("client_id"), // clientIdToCheck "/oauth2/auth", // proxyPath false, // proxyToAdmin false, // camelToSnakeCaseConversion diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java index a79ff9e92..71086161a 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java @@ -47,6 +47,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO main, req, resp, appIdentifier, storage, + null, // clientIdToCheck "/admin/clients", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java index 4faa5e65f..216073ed6 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java @@ -32,6 +32,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck "/admin/oauth2/auth/requests/consent", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java index ca9a4af6a..d432e951d 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java @@ -32,6 +32,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck "/admin/oauth2/auth/requests/login", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java index 0b4f9433f..a6e168825 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java @@ -32,6 +32,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck "/admin/oauth2/auth/requests/logout", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java index a4acb0593..e71dedb53 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java @@ -31,11 +31,11 @@ public class OAuthProxyHelper { private static final long serialVersionUID = -8734479943734920904L; public static void proxyGET(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, - String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, Map headers, GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequestForOry.Response response = OAuth.doOAuthProxyGET(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, headers); + HttpRequestForOry.Response response = OAuth.doOAuthProxyGET(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, headers); JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, @@ -57,11 +57,11 @@ public static void proxyGET(Main main, HttpServletRequest req, HttpServletRespon } public static void proxyFormPOST(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, - String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map formFields, Map headers, GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequestForOry.Response response = OAuth.doOAuthProxyFormPOST(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, formFields, headers); + HttpRequestForOry.Response response = OAuth.doOAuthProxyFormPOST(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, formFields, headers); JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, @@ -83,11 +83,11 @@ public static void proxyFormPOST(Main main, HttpServletRequest req, HttpServletR } public static void proxyJsonPOST(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, - String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, JsonObject jsonInput, Map headers, GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequestForOry.Response response = OAuth.doOAuthProxyJsonPOST(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); + HttpRequestForOry.Response response = OAuth.doOAuthProxyJsonPOST(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, @@ -109,12 +109,12 @@ public static void proxyJsonPOST(Main main, HttpServletRequest req, HttpServletR } public static void proxyJsonPUT(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, - String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, JsonObject jsonInput, Map headers, GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequestForOry.Response response = OAuth.doOAuthProxyJsonPUT(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); + HttpRequestForOry.Response response = OAuth.doOAuthProxyJsonPUT(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, @@ -136,11 +136,11 @@ public static void proxyJsonPUT(Main main, HttpServletRequest req, HttpServletRe } public static void proxyJsonDELETE(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, - String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, + String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, JsonObject jsonInput, Map headers, GetJsonResponse getJsonResponse) throws IOException, ServletException { try { - HttpRequestForOry.Response response = OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); + HttpRequestForOry.Response response = OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); JsonObject jsonResponse = getJsonResponse.apply( response.statusCode, diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java index cc886d09d..0fffe2c53 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java @@ -35,6 +35,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck "/admin/oauth2/auth/requests/consent/reject", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java index ea676b754..490bb1827 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java @@ -36,6 +36,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck "/admin/oauth2/auth/requests/login/reject", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion 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 dca66535f..8dacafc16 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java @@ -35,6 +35,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck "/admin/oauth2/auth/requests/logout/reject", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 5c64dd8b4..8660cddde 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -77,6 +77,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + formFields.get("client_id"), // clientIdToCheck "/oauth2/token", // proxyPath false, // proxyToAdmin false, // camelToSnakeCaseConversion diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java index 101f56e09..6a1a1946d 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java @@ -66,6 +66,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + null, // clientIdToCheck "/admin/oauth2/introspect", // pathProxy true, // proxyToAdmin false, // camelToSnakeCaseConversion diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java index 1063c667f..a31fb16f4 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java @@ -55,6 +55,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), + clientId, // clientIdToCheck "/admin/clients/" + clientId, // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion From b791bf4c6bc6b6ecbf84f8e0c9fd41d3382b2d2f Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 18 Sep 2024 15:07:35 +0530 Subject: [PATCH 38/40] fix: ext related --- .../java/io/supertokens/oauth/OAuthToken.java | 5 +++++ .../io/supertokens/oauth/Transformations.java | 18 ++++++++++++++++++ .../api/oauth/OAuthTokenIntrospectAPI.java | 9 ++------- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/supertokens/oauth/OAuthToken.java b/src/main/java/io/supertokens/oauth/OAuthToken.java index d2433a1a3..e04143c71 100644 --- a/src/main/java/io/supertokens/oauth/OAuthToken.java +++ b/src/main/java/io/supertokens/oauth/OAuthToken.java @@ -102,6 +102,11 @@ public static String reSignToken(AppIdentifier appIdentifier, Main main, String payload.addProperty("iss", iss); payload.addProperty("stt", tokenType.getValue()); + if (tokenType == TokenType.ACCESS_TOKEN) { + // we need to move rsub, tId and sessionHandle from ext to root + Transformations.transformExt(payload); + } + if (payloadUpdate != null) { for (Map.Entry entry : payloadUpdate.entrySet()) { if (!NON_OVERRIDABLE_TOKEN_PROPS.contains(entry.getKey())) { diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index 45df22a7a..589a553cf 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -8,6 +8,7 @@ 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; @@ -21,6 +22,7 @@ import io.supertokens.utils.Utils; public class Transformations { + private static Set EXT_PROPS = Set.of("rsub", "tId", "sessionHandle"); public static Map transformRequestHeadersForHydra(Map requestHeaders) { if (requestHeaders == null) { @@ -204,4 +206,20 @@ public static JsonObject transformJsonForHydra(JsonObject jsonInput) { } 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); + } + } + + if (ext.entrySet().size() == 0) { + payload.remove("ext"); + } + } + } } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java index 6a1a1946d..b24ca17cf 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java @@ -21,6 +21,7 @@ import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.Transformations; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -76,13 +77,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject response = jsonBody.getAsJsonObject(); response.addProperty("iss", iss); - if (response.has("ext")) { - JsonObject ext = response.get("ext").getAsJsonObject(); - for (Map.Entry entry : ext.entrySet()) { - response.add(entry.getKey(), entry.getValue()); - } - response.remove("ext"); - } + Transformations.transformExt(response); response.addProperty("status", "OK"); return response; From c568f5c869f40dfede70a2c971e7471626a0732a Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 19 Sep 2024 12:03:40 +0530 Subject: [PATCH 39/40] fix: pr comment --- .../CreateUpdateOrGetOAuthClientAPI.java | 51 ++++----- .../OAuthAcceptAuthConsentRequestAPI.java | 15 +-- .../oauth/OAuthAcceptAuthLoginRequestAPI.java | 15 +-- .../OAuthAcceptAuthLogoutRequestAPI.java | 25 ++--- .../webserver/api/oauth/OAuthAuthAPI.java | 47 ++++---- .../api/oauth/OAuthClientListAPI.java | 82 +++++++------- .../oauth/OAuthGetAuthConsentRequestAPI.java | 17 ++- .../oauth/OAuthGetAuthLoginRequestAPI.java | 17 ++- .../oauth/OAuthGetAuthLogoutRequestAPI.java | 17 ++- .../webserver/api/oauth/OAuthProxyHelper.java | 101 ++++-------------- .../OAuthRejectAuthConsentRequestAPI.java | 20 ++-- .../oauth/OAuthRejectAuthLoginRequestAPI.java | 18 ++-- .../OAuthRejectAuthLogoutRequestAPI.java | 16 +-- .../webserver/api/oauth/OAuthTokenAPI.java | 33 +++--- .../api/oauth/OAuthTokenIntrospectAPI.java | 24 +++-- .../api/oauth/RemoveOAuthClientAPI.java | 29 ++--- 16 files changed, 240 insertions(+), 287 deletions(-) diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index 3e314fa75..cfc811e8f 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -56,9 +56,9 @@ public CreateUpdateOrGetOAuthClientAPI(Main main){ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { String clientId = InputParser.getQueryParamOrThrowError(req, "clientId", false); - + try { - OAuthProxyHelper.proxyGET( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -67,11 +67,11 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO true, // proxyToAdmin true, // camelToSnakeCaseConversion OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - new HashMap<>(), // getHeadersForProxy - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - return jsonBody.getAsJsonObject(); - } + new HashMap<>() ); + if (response != null) { + super.sendJsonResponse(200, response.jsonResponse, resp); + } } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); } @@ -92,7 +92,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I input.addProperty("owner", appIdentifier.getAppId()); - OAuthProxyHelper.proxyJsonPOST( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPOST( main, req, resp, appIdentifier, storage, @@ -101,20 +101,20 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I true, // proxyToAdmin true, // camelToSnakeCaseConversion input, // jsonBody - new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - String clientId = jsonBody.getAsJsonObject().get("clientId").getAsString(); - - try { - OAuth.addClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); - } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { - throw new ServletException(e); - } catch (OAuth2ClientAlreadyExistsForAppException e) { - // ignore - } - return jsonBody.getAsJsonObject(); - } + new HashMap<>() // headers ); + if (response != null) { + String clientId = response.jsonResponse.getAsJsonObject().get("clientId").getAsString(); + + try { + OAuth.addClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } catch (OAuth2ClientAlreadyExistsForAppException e) { + // ignore + } + super.sendJsonResponse(200, response.jsonResponse, resp); + } } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); } @@ -151,7 +151,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO } try { - OAuthProxyHelper.proxyJsonPUT( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -161,11 +161,12 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO true, // camelToSnakeCaseConversion new HashMap<>(), // queryParams input, // jsonBody - new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - return jsonBody.getAsJsonObject(); - } + new HashMap<>() // headers ); + + if (response != null) { + 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 index 252af8542..3df0236d0 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java @@ -7,6 +7,7 @@ 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; @@ -31,7 +32,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject input = InputParser.parseJsonObjectOrThrowError(req); try { - OAuthProxyHelper.proxyJsonPUT( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -41,13 +42,13 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO true, // camelToSnakeCaseConversion OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), input, // jsonBody - new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - return response; - } + 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 index ea73b7bf4..792f01539 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLoginRequestAPI.java @@ -9,6 +9,7 @@ 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; @@ -31,7 +32,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject input = InputParser.parseJsonObjectOrThrowError(req); try { - OAuthProxyHelper.proxyJsonPUT( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -41,13 +42,13 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO true, OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), input, // jsonBody - new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - return response; - } + 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 index c321b50bf..7d07c254d 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthLogoutRequestAPI.java @@ -7,6 +7,7 @@ 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; @@ -31,23 +32,23 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject input = InputParser.parseJsonObjectOrThrowError(req); try { - OAuthProxyHelper.proxyJsonPUT( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), null, // clientIdToCheck - "/admin/oauth2/auth/requests/logout/accept", - true, - true, - OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - input, - new HashMap<>(), - (statusCode, headers, rawBody, jsonBody) -> { - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - return response; - } + "/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 0662d0405..c7b604801 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -22,6 +22,7 @@ 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; @@ -64,7 +65,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } try { - OAuthProxyHelper.proxyGET( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -73,30 +74,32 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I false, // proxyToAdmin false, // camelToSnakeCaseConversion queryParams, - headers, - (statusCode, responseHeaders, rawBody, jsonBody) -> { // getJsonResponse - if (headers == null || !responseHeaders.containsKey("Location")) { - throw new IllegalStateException("Invalid response from hydra"); - } - - String redirectTo = responseHeaders.get("Location").get(0); - List responseCookies = responseHeaders.get("Set-Cookie"); - - JsonObject response = new JsonObject(); - response.addProperty("redirectTo", redirectTo); + headers + ); - JsonArray jsonCookies = new JsonArray(); - if (responseCookies != null) { - for (String cookie : responseCookies) { - jsonCookies.add(new JsonPrimitive(cookie)); - } + if (response != null) { + if (response.headers == null || !response.headers.containsKey("Location")) { + throw new IllegalStateException("Invalid response from hydra"); + } + + String redirectTo = response.headers.get("Location").get(0); + List responseCookies = response.headers.get("Set-Cookie"); + + JsonObject finalResponse = new JsonObject(); + finalResponse.addProperty("redirectTo", redirectTo); + + JsonArray jsonCookies = new JsonArray(); + if (responseCookies != null) { + for (String cookie : responseCookies) { + jsonCookies.add(new JsonPrimitive(cookie)); } - - response.add("cookies", jsonCookies); - response.addProperty("status", "OK"); - return response; } - ); + + finalResponse.add("cookies", jsonCookies); + 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/OAuthClientListAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java index 71086161a..00eff45d3 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientListAPI.java @@ -13,6 +13,7 @@ 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; @@ -43,7 +44,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO Map queryParams = OAuthProxyHelper.defaultGetQueryParamsFromRequest(req); queryParams.put("owner", appIdentifier.getAppId()); - OAuthProxyHelper.proxyGET( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( main, req, resp, appIdentifier, storage, @@ -52,56 +53,57 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO true, // proxyToAdmin true, // camelToSnakeCaseConversion queryParams, - new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - JsonObject response = new JsonObject(); - response.addProperty("status", "OK"); + new HashMap<>() // headers + ); - // 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); - } + if (response != null) { + JsonObject finalResponse = new JsonObject(); + finalResponse.addProperty("status", "OK"); - Set clientIdsSet = new HashSet<>(clientIds); + // 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); + } - JsonArray clients = new JsonArray(); - - for (JsonElement clientElem : jsonBody.getAsJsonArray()) { - if (clientIdsSet.contains(clientElem.getAsJsonObject().get("clientId").getAsString())) { - clients.add(clientElem); - } + 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); } + } - response.add("clients", clients); + finalResponse.add("clients", clients); - // pagination - List linkHeader = headers.get("Link"); - if (linkHeader != null && !linkHeader.isEmpty()) { - for (String nextLink : linkHeader.get(0).split(",")) { - if (!nextLink.contains("rel=\"next\"")) { - continue; - } + // 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) { - response.addProperty("nextPaginationToken", pageToken); + 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); + } } - - return response; } - ); + + 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/OAuthGetAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java index 216073ed6..fa40aec62 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java @@ -3,10 +3,9 @@ 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.WebserverAPI; @@ -28,7 +27,7 @@ public String getPath() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { try { - OAuthProxyHelper.proxyGET( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -37,14 +36,14 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO true, // proxyToAdmin true, // camelToSnakeCaseConversion OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - return response; - } + 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/OAuthGetAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java index d432e951d..d508369bc 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java @@ -3,10 +3,9 @@ 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.WebserverAPI; @@ -28,7 +27,7 @@ public String getPath() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { try { - OAuthProxyHelper.proxyGET( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -37,14 +36,14 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO true, // proxyToAdmin true, // camelToSnakeCaseConversion OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - return response; - } + 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/OAuthGetAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java index a6e168825..ad174654c 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java @@ -3,10 +3,9 @@ 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.WebserverAPI; @@ -28,7 +27,7 @@ public String getPath() { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { try { - OAuthProxyHelper.proxyGET( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -37,14 +36,14 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO true, // proxyToAdmin true, // camelToSnakeCaseConversion OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - return response; - } + 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/OAuthProxyHelper.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java index e71dedb53..3a30544d9 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java @@ -5,10 +5,8 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.List; import java.util.Map; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.Main; @@ -30,22 +28,11 @@ public class OAuthProxyHelper { @Serial private static final long serialVersionUID = -8734479943734920904L; - public static void proxyGET(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + 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, - GetJsonResponse getJsonResponse) throws IOException, ServletException { + Map queryParams, Map headers) throws IOException, ServletException { try { - HttpRequestForOry.Response response = OAuth.doOAuthProxyGET(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, headers); - - JsonObject jsonResponse = getJsonResponse.apply( - response.statusCode, - response.headers, - response.rawResponse, - response.jsonResponse - ); - resp.setStatus(200); - resp.setHeader("Content-Type", "application/json; charset=UTF-8"); - resp.getWriter().println(jsonResponse.toString()); + return OAuth.doOAuthProxyGET(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, headers); } catch (OAuthClientNotFoundException e) { handleOAuthClientNotFoundException(resp); @@ -54,25 +41,14 @@ public static void proxyGET(Main main, HttpServletRequest req, HttpServletRespon } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { throw new ServletException(e); } + return null; } - public static void proxyFormPOST(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + 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, - GetJsonResponse getJsonResponse) throws IOException, ServletException { + Map formFields, Map headers) throws IOException, ServletException { try { - HttpRequestForOry.Response response = OAuth.doOAuthProxyFormPOST(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, formFields, headers); - - JsonObject jsonResponse = getJsonResponse.apply( - response.statusCode, - response.headers, - response.rawResponse, - response.jsonResponse - ); - resp.setStatus(200); - resp.setHeader("Content-Type", "application/json; charset=UTF-8"); - resp.getWriter().println(jsonResponse.toString()); - + return OAuth.doOAuthProxyFormPOST(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, formFields, headers); } catch (OAuthClientNotFoundException e) { handleOAuthClientNotFoundException(resp); } catch (OAuthAPIException e) { @@ -80,25 +56,14 @@ public static void proxyFormPOST(Main main, HttpServletRequest req, HttpServletR } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { throw new ServletException(e); } + return null; } - public static void proxyJsonPOST(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + 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, - GetJsonResponse getJsonResponse) throws IOException, ServletException { + JsonObject jsonInput, Map headers) throws IOException, ServletException { try { - HttpRequestForOry.Response response = OAuth.doOAuthProxyJsonPOST(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); - - JsonObject jsonResponse = getJsonResponse.apply( - response.statusCode, - response.headers, - response.rawResponse, - response.jsonResponse - ); - resp.setStatus(200); - resp.setHeader("Content-Type", "application/json; charset=UTF-8"); - resp.getWriter().println(jsonResponse.toString()); - + return OAuth.doOAuthProxyJsonPOST(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); } catch (OAuthClientNotFoundException e) { handleOAuthClientNotFoundException(resp); } catch (OAuthAPIException e) { @@ -106,26 +71,15 @@ public static void proxyJsonPOST(Main main, HttpServletRequest req, HttpServletR } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { throw new ServletException(e); } + return null; } - public static void proxyJsonPUT(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + 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, GetJsonResponse getJsonResponse) throws IOException, ServletException { + Map queryParams, JsonObject jsonInput, Map headers) throws IOException, ServletException { try { - HttpRequestForOry.Response response = OAuth.doOAuthProxyJsonPUT(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); - - JsonObject jsonResponse = getJsonResponse.apply( - response.statusCode, - response.headers, - response.rawResponse, - response.jsonResponse - ); - resp.setStatus(200); - resp.setHeader("Content-Type", "application/json; charset=UTF-8"); - resp.getWriter().println(jsonResponse.toString()); - + return OAuth.doOAuthProxyJsonPUT(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); } catch (OAuthClientNotFoundException e) { handleOAuthClientNotFoundException(resp); } catch (OAuthAPIException e) { @@ -133,25 +87,14 @@ public static void proxyJsonPUT(Main main, HttpServletRequest req, HttpServletRe } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { throw new ServletException(e); } + return null; } - public static void proxyJsonDELETE(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, + public static HttpRequestForOry.Response proxyJsonDELETE(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, - JsonObject jsonInput, Map headers, - GetJsonResponse getJsonResponse) throws IOException, ServletException { + JsonObject jsonInput, Map headers) throws IOException, ServletException { try { - HttpRequestForOry.Response response = OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); - - JsonObject jsonResponse = getJsonResponse.apply( - response.statusCode, - response.headers, - response.rawResponse, - response.jsonResponse - ); - resp.setStatus(200); - resp.setHeader("Content-Type", "application/json; charset=UTF-8"); - resp.getWriter().println(jsonResponse.toString()); - + return OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); } catch (OAuthClientNotFoundException e) { handleOAuthClientNotFoundException(resp); } catch (OAuthAPIException e) { @@ -159,6 +102,7 @@ public static void proxyJsonDELETE(Main main, HttpServletRequest req, HttpServle } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | InvalidConfigException e) { throw new ServletException(e); } + return null; } public static Map defaultGetQueryParamsFromRequest(HttpServletRequest req) { @@ -178,11 +122,6 @@ public static Map defaultGetQueryParamsFromRequest(HttpServletRe return queryParams; } - @FunctionalInterface - public interface GetJsonResponse { - JsonObject apply(int statusCode, Map> headers, String rawBody, JsonElement jsonBody) throws IOException, ServletException; - } - private static void handleOAuthClientNotFoundException(HttpServletResponse resp) throws IOException { JsonObject response = new JsonObject(); response.addProperty("status", "CLIENT_NOT_FOUND_ERROR"); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java index 0fffe2c53..ef4fed870 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthConsentRequestAPI.java @@ -7,6 +7,7 @@ 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; @@ -31,7 +32,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject input = InputParser.parseJsonObjectOrThrowError(req); try { - OAuthProxyHelper.proxyJsonPUT( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -39,15 +40,16 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/oauth2/auth/requests/consent/reject", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), - input, // getJsonBody - new HashMap<>(), // getHeadersForProxy - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - return response; - } + 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 index 490bb1827..6462d358c 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLoginRequestAPI.java @@ -8,6 +8,7 @@ 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; @@ -32,7 +33,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject input = InputParser.parseJsonObjectOrThrowError(req); try { - OAuthProxyHelper.proxyJsonPUT( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -40,15 +41,16 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO "/admin/oauth2/auth/requests/login/reject", // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), + OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), // queryParams input, // jsonBody - new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - return response; - } + 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 index 8dacafc16..10252c230 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthRejectAuthLogoutRequestAPI.java @@ -7,6 +7,7 @@ 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; @@ -31,7 +32,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject input = InputParser.parseJsonObjectOrThrowError(req); try { - OAuthProxyHelper.proxyJsonPUT( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -41,13 +42,14 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO true, // camelToSnakeCaseConversion OAuthProxyHelper.defaultGetQueryParamsFromRequest(req), input, // jsonBody - new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - JsonObject response = jsonBody.getAsJsonObject(); - response.addProperty("status", "OK"); - return response; - } + 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 index 8660cddde..22a479c6a 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -21,6 +21,7 @@ import io.supertokens.Main; import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; import io.supertokens.oauth.OAuth; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; @@ -73,7 +74,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } try { - OAuthProxyHelper.proxyFormPOST( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -82,26 +83,22 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I false, // proxyToAdmin false, // camelToSnakeCaseConversion formFields, - new HashMap<>(), // headers - (statusCode, headers, rawBody, jsonBody) -> { - if (jsonBody == null) { - throw new IllegalStateException("unexpected response from hydra"); - } - - try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + new HashMap<>() // headers + ); - jsonBody = OAuth.transformTokens(super.main, appIdentifier, storage, jsonBody.getAsJsonObject(), iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); - - } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { - throw new ServletException(e); - } + if (response != null) { + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - jsonBody.getAsJsonObject().addProperty("status", "OK"); - return jsonBody.getAsJsonObject(); + response.jsonResponse = OAuth.transformTokens(super.main, appIdentifier, storage, response.jsonResponse.getAsJsonObject(), iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); + } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { + 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 index b24ca17cf..902e7de83 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java @@ -20,6 +20,7 @@ import io.supertokens.Main; import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; import io.supertokens.oauth.OAuth; import io.supertokens.oauth.Transformations; import io.supertokens.pluginInterface.RECIPE_ID; @@ -63,7 +64,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } try { - OAuthProxyHelper.proxyFormPOST( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -71,18 +72,19 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I "/admin/oauth2/introspect", // pathProxy true, // proxyToAdmin false, // camelToSnakeCaseConversion - formFields, - new HashMap<>(), // getHeaders - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - JsonObject response = jsonBody.getAsJsonObject(); + formFields, // formFields + new HashMap<>() // headers + ); - response.addProperty("iss", iss); - Transformations.transformExt(response); + if (response != null) { + JsonObject finalResponse = response.jsonResponse.getAsJsonObject(); - response.addProperty("status", "OK"); - return response; - } - ); + finalResponse.addProperty("iss", iss); + Transformations.transformExt(finalResponse); + + 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/RemoveOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java index a31fb16f4..9e7f00c9b 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java @@ -24,6 +24,7 @@ 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; @@ -51,7 +52,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String clientId = InputParser.parseStringOrThrowError(input, "clientId", false); try { - OAuthProxyHelper.proxyJsonDELETE( + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonDELETE( main, req, resp, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), @@ -59,20 +60,22 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I "/admin/clients/" + clientId, // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion - new JsonObject(), // getJsonBody - new HashMap<>(), // getHeadersForProxy - (statusCode, headers, rawBody, jsonBody) -> { // getJsonResponse - try { - OAuth.removeClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); - } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { - throw new ServletException(e); - } + new JsonObject(), // jsonBody + new HashMap<>() // headers + ); - JsonObject response = new JsonObject(); - response.addProperty("status", "OK"); - return response; + 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); From bd86375de4b9acf7ab5be78e19751079661c6f96 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 25 Sep 2024 16:10:57 +0530 Subject: [PATCH 40/40] fix: revoke APIs (#1041) * fix: revoke consent sessions * fix: revoke token * fix: revoke impl * fix: revoke session * fix: introspect impl after revoke * fix: revoke by client_id * fix: refresh token check in token exchange * fix: at hash * fix: sqlite impl * fix: client props whitelist * fix: status and query null check * fix: plugin interface update * fix: logout api * fix: ext * fix: accept consent * fix: accept consent * fix: introspect in token api * fix: keep fragment while updating query params * fix: count creds and pr comment * fix: oauth stats * fix: oauth cleanup cron task * fix: gid in refresh token * fix: inememory impl * feat: add initial payload fields to accept consent * fix: revoke cleanup * fix: stats * fix: client credentials basic * fix: authorization header * fix: authorizaion header in revoke * fix: missing table --------- Co-authored-by: Mihaly Lengyel --- .../java/io/supertokens/ee/EEFeatureFlag.java | 28 ++ src/main/java/io/supertokens/Main.java | 3 + .../CleanupOAuthRevokeList.java | 57 ++++ .../java/io/supertokens/inmemorydb/Start.java | 96 ++++++- .../inmemorydb/config/SQLiteConfig.java | 12 +- .../inmemorydb/queries/GeneralQueries.java | 19 +- .../inmemorydb/queries/OAuthQueries.java | 210 ++++++++++++++- .../supertokens/oauth/HttpRequestForOry.java | 10 +- src/main/java/io/supertokens/oauth/OAuth.java | 250 ++++++++++++++++-- .../java/io/supertokens/oauth/OAuthToken.java | 21 +- .../io/supertokens/oauth/Transformations.java | 69 ++++- .../io/supertokens/webserver/Webserver.java | 21 +- .../CreateUpdateOrGetOAuthClientAPI.java | 30 ++- .../OAuthAcceptAuthConsentRequestAPI.java | 31 +++ .../webserver/api/oauth/OAuthAuthAPI.java | 21 +- .../oauth/OAuthGetAuthConsentRequestAPI.java | 3 + .../oauth/OAuthGetAuthLoginRequestAPI.java | 3 + .../oauth/OAuthGetAuthLogoutRequestAPI.java | 3 + .../webserver/api/oauth/OAuthLogoutAPI.java | 73 +++++ .../webserver/api/oauth/OAuthProxyHelper.java | 6 +- .../webserver/api/oauth/OAuthTokenAPI.java | 74 +++++- .../api/oauth/OAuthTokenIntrospectAPI.java | 21 +- .../api/oauth/RemoveOAuthClientAPI.java | 1 + .../api/oauth/RevokeOAuthSessionAPI.java | 50 ++++ .../api/oauth/RevokeOAuthTokenAPI.java | 141 ++++++++++ .../api/oauth/RevokeOAuthTokensAPI.java | 70 +++++ .../test/oauth/api/OAuthAuthAPITest.java | 1 - .../test/oauth/api/OAuthClientsAPITest.java | 1 - 28 files changed, 1220 insertions(+), 105 deletions(-) create mode 100644 src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthSessionAPI.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index 4c959f6f8..e01b8ed26 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -24,6 +24,7 @@ import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.StorageUtils; import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; import io.supertokens.pluginInterface.dashboard.sqlStorage.DashboardSQLStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -32,6 +33,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.ThirdPartyConfig; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.utils.Utils; @@ -338,6 +340,28 @@ private JsonObject getAccountLinkingStats() throws StorageQueryException, Tenant return result; } + private JsonObject getOAuthStats() throws StorageQueryException, TenantOrAppNotFoundException { + JsonObject result = new JsonObject(); + + OAuthStorage oAuthStorage = StorageUtils.getOAuthStorage(StorageLayer.getStorage( + this.appIdentifier.getAsPublicTenantIdentifier(), main)); + + result.addProperty("totalNumberOfClients", oAuthStorage.countTotalNumberOfClientsForApp(appIdentifier)); + result.addProperty("numberOfClientCredentialsOnlyClients", oAuthStorage.countTotalNumberOfClientCredentialsOnlyClientsForApp(appIdentifier)); + result.addProperty("numberOfM2MTokensAlive", oAuthStorage.countTotalNumberOfM2MTokensAlive(appIdentifier)); + + long now = System.currentTimeMillis(); + JsonArray tokensCreatedArray = new JsonArray(); + for (int i = 1; i <= 31; i++) { + long timestamp = now - (i * 24 * 60 * 60 * 1000L); + int numberOfTokensCreated = oAuthStorage.countTotalNumberOfM2MTokensCreatedSince(this.appIdentifier, timestamp); + tokensCreatedArray.add(new JsonPrimitive(numberOfTokensCreated)); + } + result.add("numberOfM2MTokensCreated", tokensCreatedArray); + + return result; + } + private JsonArray getMAUs() throws StorageQueryException, TenantOrAppNotFoundException { JsonArray mauArr = new JsonArray(); long now = System.currentTimeMillis(); @@ -395,6 +419,10 @@ public JsonObject getPaidFeatureStats() throws StorageQueryException, TenantOrAp if (feature == EE_FEATURES.SECURITY) { usageStats.add(EE_FEATURES.SECURITY.toString(), new JsonObject()); } + + if (feature == EE_FEATURES.OAUTH) { + usageStats.add(EE_FEATURES.OAUTH.toString(), getOAuthStats()); + } } usageStats.add("maus", getMAUs()); diff --git a/src/main/java/io/supertokens/Main.java b/src/main/java/io/supertokens/Main.java index f6e9f29ea..1eef7e500 100644 --- a/src/main/java/io/supertokens/Main.java +++ b/src/main/java/io/supertokens/Main.java @@ -20,6 +20,7 @@ import io.supertokens.config.Config; import io.supertokens.config.CoreConfig; import io.supertokens.cronjobs.Cronjobs; +import io.supertokens.cronjobs.cleanupOAuthRevokeList.CleanupOAuthRevokeList; import io.supertokens.cronjobs.deleteExpiredAccessTokenSigningKeys.DeleteExpiredAccessTokenSigningKeys; import io.supertokens.cronjobs.deleteExpiredDashboardSessions.DeleteExpiredDashboardSessions; import io.supertokens.cronjobs.deleteExpiredEmailVerificationTokens.DeleteExpiredEmailVerificationTokens; @@ -256,6 +257,8 @@ private void init() throws IOException, StorageQueryException { // starts DeleteExpiredAccessTokenSigningKeys cronjob if the access token signing keys can change Cronjobs.addCronjob(this, DeleteExpiredAccessTokenSigningKeys.init(this, uniqueUserPoolIdsTenants)); + Cronjobs.addCronjob(this, CleanupOAuthRevokeList.init(this, uniqueUserPoolIdsTenants)); + // this is to ensure tenantInfos are in sync for the new cron job as well MultitenancyHelper.getInstance(this).refreshCronjobs(); diff --git a/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java b/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java new file mode 100644 index 000000000..a94d65d51 --- /dev/null +++ b/src/main/java/io/supertokens/cronjobs/cleanupOAuthRevokeList/CleanupOAuthRevokeList.java @@ -0,0 +1,57 @@ +package io.supertokens.cronjobs.cleanupOAuthRevokeList; + +import java.util.List; + +import io.supertokens.Main; +import io.supertokens.cronjobs.CronTask; +import io.supertokens.cronjobs.CronTaskTest; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.StorageUtils; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.oauth.OAuthStorage; +import io.supertokens.storageLayer.StorageLayer; + +public class CleanupOAuthRevokeList extends CronTask { + + public static final String RESOURCE_KEY = "io.supertokens.cronjobs.cleanupOAuthRevokeList" + + ".CleanupOAuthRevokeList"; + + private CleanupOAuthRevokeList(Main main, List> tenantsInfo) { + super("CleanupOAuthRevokeList", main, tenantsInfo, true); + } + + public static CleanupOAuthRevokeList init(Main main, List> tenantsInfo) { + return (CleanupOAuthRevokeList) main.getResourceDistributor() + .setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY, + new CleanupOAuthRevokeList(main, tenantsInfo)); + } + + @Override + protected void doTaskPerApp(AppIdentifier app) throws Exception { + Storage storage = StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main); + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.cleanUpExpiredAndRevokedTokens(app); + } + + @Override + public int getIntervalTimeSeconds() { + if (Main.isTesting) { + Integer interval = CronTaskTest.getInstance(main).getIntervalInSeconds(RESOURCE_KEY); + if (interval != null) { + return interval; + } + } + // Every 24 hours. + return 24 * 3600; + } + + @Override + public int getInitialWaitTimeSeconds() { + if (!Main.isTesting) { + return getIntervalTimeSeconds(); + } else { + return 0; + } + } +} diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 9cb99abbc..288e6593e 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -55,7 +55,6 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.multitenancy.sqlStorage.MultitenancySQLStorage; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; @@ -107,7 +106,6 @@ public class Start ActiveUsersSQLStorage, DashboardSQLStorage, AuthRecipeSQLStorage, OAuthSQLStorage { private static final Object appenderLock = new Object(); - private static final String APP_ID_KEY_NAME = "app_id"; private static final String ACCESS_TOKEN_SIGNING_KEY_NAME = "access_token_signing_key"; private static final String REFRESH_TOKEN_KEY_NAME = "refresh_token_key"; public static boolean isTesting = false; @@ -3011,7 +3009,7 @@ public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(A } @Override - public boolean doesClientIdExistForThisApp(AppIdentifier appIdentifier, String clientId) + public boolean doesClientIdExistForApp(AppIdentifier appIdentifier, String clientId) throws StorageQueryException { try { return OAuthQueries.isClientIdForAppId(this, clientId, appIdentifier); @@ -3021,19 +3019,11 @@ public boolean doesClientIdExistForThisApp(AppIdentifier appIdentifier, String c } @Override - public void addClientForApp(AppIdentifier appIdentifier, String clientId) - throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException { + public void addOrUpdateClientForApp(AppIdentifier appIdentifier, String clientId, boolean isClientCredentialsOnly) + throws StorageQueryException { try { - OAuthQueries.insertClientIdForAppId(this, clientId, appIdentifier); + OAuthQueries.insertClientIdForAppId(this, appIdentifier, clientId, isClientCredentialsOnly); } catch (SQLException e) { - - SQLiteConfig config = Config.getConfig(this); - String serverErrorMessage = e.getMessage(); - - if (isPrimaryKeyError(serverErrorMessage, config.getOAuthClientTable(), - new String[]{"app_id", "client_id"})) { - throw new OAuth2ClientAlreadyExistsForAppException(); - } throw new StorageQueryException(e); } } @@ -3055,4 +3045,82 @@ public List listClientsForApp(AppIdentifier appIdentifier) throws Storag throw new StorageQueryException(e); } } + + @Override + public void revoke(AppIdentifier appIdentifier, String targetType, String targetValue, long exp) + throws StorageQueryException { + try { + OAuthQueries.revoke(this, appIdentifier, targetType, targetValue, exp); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + + } + + @Override + public boolean isRevoked(AppIdentifier appIdentifier, String[] targetTypes, String[] targetValues, long issuedAt) + throws StorageQueryException { + try { + return OAuthQueries.isRevoked(this, appIdentifier, targetTypes, targetValues, issuedAt); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void addM2MToken(AppIdentifier appIdentifier, String clientId, long iat, long exp) + throws StorageQueryException { + try { + OAuthQueries.addM2MToken(this, appIdentifier, clientId, iat, exp); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void cleanUpExpiredAndRevokedTokens(AppIdentifier appIdentifier) throws StorageQueryException { + try { + OAuthQueries.cleanUpExpiredAndRevokedTokens(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfClientCredentialsOnlyClientsForApp(AppIdentifier appIdentifier) + throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfClientsForApp(this, appIdentifier, true); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfClientsForApp(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfClientsForApp(this, appIdentifier, false); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfM2MTokensAlive(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfM2MTokensAlive(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countTotalNumberOfM2MTokensCreatedSince(AppIdentifier appIdentifier, long since) + throws StorageQueryException { + try { + return OAuthQueries.countTotalNumberOfM2MTokensCreatedSince(this, appIdentifier, since); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } } diff --git a/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java b/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java index bc969dc6f..c14646456 100644 --- a/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java +++ b/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java @@ -165,5 +165,15 @@ public String getDashboardSessionsTable() { return "dashboard_user_sessions"; } - public String getOAuthClientTable(){ return "oauth_clients"; } + public String getOAuthClientsTable() { + return "oauth_clients"; + } + + public String getOAuthRevokeTable() { + return "oauth_revoke"; + } + + public String getOAuthM2MTokensTable() { + return "oauth_m2m_tokens"; + } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java index 665511467..13d4ee092 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java @@ -423,10 +423,27 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc update(start, TOTPQueries.getQueryToCreateUsedCodesExpiryTimeIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getOAuthClientTable())) { + if (!doesTableExists(start, Config.getConfig(start).getOAuthClientsTable())) { getInstance(main).addState(CREATING_NEW_TABLE, null); update(start, OAuthQueries.getQueryToCreateOAuthClientTable(start), NO_OP_SETTER); } + + if (!doesTableExists(start, Config.getConfig(start).getOAuthRevokeTable())) { + getInstance(main).addState(CREATING_NEW_TABLE, null); + update(start, OAuthQueries.getQueryToCreateOAuthRevokeTable(start), NO_OP_SETTER); + + // index + update(start, OAuthQueries.getQueryToCreateOAuthRevokeTimestampIndex(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, Config.getConfig(start).getOAuthM2MTokensTable())) { + getInstance(main).addState(CREATING_NEW_TABLE, null); + update(start, OAuthQueries.getQueryToCreateOAuthM2MTokensTable(start), NO_OP_SETTER); + + // index + update(start, OAuthQueries.getQueryToCreateOAuthM2MTokenIatIndex(start), NO_OP_SETTER); + update(start, OAuthQueries.getQueryToCreateOAuthM2MTokenExpIndex(start), NO_OP_SETTER); + } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java index 806fc9c6d..5a27ee8d5 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java @@ -32,19 +32,69 @@ public class OAuthQueries { public static String getQueryToCreateOAuthClientTable(Start start) { - String oAuth2ClientTable = Config.getConfig(start).getOAuthClientTable(); + String oAuth2ClientTable = Config.getConfig(start).getOAuthClientsTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + oAuth2ClientTable + " (" + "app_id VARCHAR(64)," + "client_id VARCHAR(128) NOT NULL," + + "is_client_credentials_only BOOLEAN NOT NULL," + " PRIMARY KEY (app_id, client_id)," + " FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE);"; // @formatter:on } + public static String getQueryToCreateOAuthRevokeTable(Start start) { + String oAuth2RevokeTable = Config.getConfig(start).getOAuthRevokeTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + oAuth2RevokeTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "target_type VARCHAR(16) NOT NULL," + + "target_value VARCHAR(128) NOT NULL," + + "timestamp BIGINT NOT NULL, " + + "exp BIGINT NOT NULL," + + "PRIMARY KEY (app_id, target_type, target_value)," + + "FOREIGN KEY(app_id) " + + " REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + public static String getQueryToCreateOAuthRevokeTimestampIndex(Start start) { + String oAuth2RevokeTable = Config.getConfig(start).getOAuthRevokeTable(); + return "CREATE INDEX IF NOT EXISTS oauth_revoke_timestamp_index ON " + + oAuth2RevokeTable + "(timestamp DESC, app_id DESC);"; + } + + public static String getQueryToCreateOAuthM2MTokensTable(Start start) { + String oAuth2M2MTokensTable = Config.getConfig(start).getOAuthM2MTokensTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + oAuth2M2MTokensTable + " (" + + "app_id VARCHAR(64) DEFAULT 'public'," + + "client_id VARCHAR(128) NOT NULL," + + "iat BIGINT NOT NULL," + + "exp BIGINT NOT NULL," + + "PRIMARY KEY (app_id, client_id, iat)," + + "FOREIGN KEY(app_id)" + + " REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + public static String getQueryToCreateOAuthM2MTokenIatIndex(Start start) { + String oAuth2M2MTokensTable = Config.getConfig(start).getOAuthM2MTokensTable(); + return "CREATE INDEX IF NOT EXISTS oauth_m2m_token_iat_index ON " + + oAuth2M2MTokensTable + "(iat DESC, app_id DESC);"; + } + + public static String getQueryToCreateOAuthM2MTokenExpIndex(Start start) { + String oAuth2M2MTokensTable = Config.getConfig(start).getOAuthM2MTokensTable(); + return "CREATE INDEX IF NOT EXISTS oauth_m2m_token_exp_index ON " + + oAuth2M2MTokensTable + "(exp DESC, app_id DESC);"; + } + public static boolean isClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthClientTable() + + String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthClientsTable() + " WHERE client_id = ? AND app_id = ?"; return execute(start, QUERY, pst -> { @@ -55,7 +105,7 @@ public static boolean isClientIdForAppId(Start start, String clientId, AppIdenti public static List listClientsForApp(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - String QUERY = "SELECT client_id FROM " + Config.getConfig(start).getOAuthClientTable() + + String QUERY = "SELECT client_id FROM " + Config.getConfig(start).getOAuthClientsTable() + " WHERE app_id = ?"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -68,19 +118,23 @@ public static List listClientsForApp(Start start, AppIdentifier appIdent }); } - public static void insertClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier) + public static void insertClientIdForAppId(Start start, AppIdentifier appIdentifier, String clientId, + boolean isClientCredentialsOnly) throws SQLException, StorageQueryException { - String INSERT = "INSERT INTO " + Config.getConfig(start).getOAuthClientTable() - + "(app_id, client_id) VALUES(?, ?)"; + String INSERT = "INSERT INTO " + Config.getConfig(start).getOAuthClientsTable() + + "(app_id, client_id, is_client_credentials_only) VALUES(?, ?, ?) " + + "ON CONFLICT (app_id, client_id) DO UPDATE SET is_client_credentials_only = ?"; update(start, INSERT, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, clientId); + pst.setBoolean(3, isClientCredentialsOnly); + pst.setBoolean(4, isClientCredentialsOnly); }); } public static boolean deleteClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthClientTable() + String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthClientsTable() + " WHERE app_id = ? AND client_id = ?"; int numberOfRow = update(start, DELETE, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -89,4 +143,146 @@ public static boolean deleteClientIdForAppId(Start start, String clientId, AppId return numberOfRow > 0; } + public static void revoke(Start start, AppIdentifier appIdentifier, String targetType, String targetValue, long exp) + throws SQLException, StorageQueryException { + String INSERT = "INSERT INTO " + Config.getConfig(start).getOAuthRevokeTable() + + "(app_id, target_type, target_value, timestamp, exp) VALUES (?, ?, ?, ?, ?) " + + "ON CONFLICT (app_id, target_type, target_value) DO UPDATE SET timestamp = ?, exp = ?"; + + long currentTime = System.currentTimeMillis() / 1000; + update(start, INSERT, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, targetType); + pst.setString(3, targetValue); + pst.setLong(4, currentTime); + pst.setLong(5, exp); + pst.setLong(6, currentTime); + pst.setLong(7, exp); + }); + } + + public static boolean isRevoked(Start start, AppIdentifier appIdentifier, String[] targetTypes, String[] targetValues, long issuedAt) + throws SQLException, StorageQueryException { + String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthRevokeTable() + + " WHERE app_id = ? AND timestamp > ? AND ("; + + for (int i = 0; i < targetTypes.length; i++) { + QUERY += "(target_type = ? AND target_value = ?)"; + + if (i < targetTypes.length - 1) { + QUERY += " OR "; + } + } + + QUERY += ")"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, issuedAt); + + int index = 3; + for (int i = 0; i < targetTypes.length; i++) { + pst.setString(index, targetTypes[i]); + index++; + pst.setString(index, targetValues[i]); + index++; + } + }, ResultSet::next); + } + + public static int countTotalNumberOfClientsForApp(Start start, AppIdentifier appIdentifier, + boolean filterByClientCredentialsOnly) throws SQLException, StorageQueryException { + if (filterByClientCredentialsOnly) { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE app_id = ? AND is_client_credentials_only = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setBoolean(2, true); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } else { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthClientsTable() + + " WHERE app_id = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + } + + public static int countTotalNumberOfM2MTokensAlive(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " WHERE app_id = ? AND exp > ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, System.currentTimeMillis()/1000); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + + public static int countTotalNumberOfM2MTokensCreatedSince(Start start, AppIdentifier appIdentifier, long since) + throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as c FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " WHERE app_id = ? AND iat >= ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, since / 1000); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + + public static void addM2MToken(Start start, AppIdentifier appIdentifier, String clientId, long iat, long exp) + throws SQLException, StorageQueryException { + String QUERY = "INSERT INTO " + Config.getConfig(start).getOAuthM2MTokensTable() + + " (app_id, client_id, iat, exp) VALUES (?, ?, ?, ?)"; + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, clientId); + pst.setLong(3, iat); + pst.setLong(4, exp); + }); + } + + public static void cleanUpExpiredAndRevokedTokens(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { + { + // delete expired M2M tokens + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthM2MTokensTable() + + " WHERE app_id = ? AND exp < ?"; + + long timestamp = System.currentTimeMillis() / 1000 - 3600 * 24 * 31; // expired 31 days ago + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, timestamp); + }); + } + + { + // delete expired revoked tokens + String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthRevokeTable() + + " WHERE app_id = ? AND exp < ?"; + + long timestamp = System.currentTimeMillis() / 1000 - 3600 * 24 * 31; // expired 31 days ago + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, timestamp); + }); + } + } } diff --git a/src/main/java/io/supertokens/oauth/HttpRequestForOry.java b/src/main/java/io/supertokens/oauth/HttpRequestForOry.java index 62f5945e8..56e92bf55 100644 --- a/src/main/java/io/supertokens/oauth/HttpRequestForOry.java +++ b/src/main/java/io/supertokens/oauth/HttpRequestForOry.java @@ -127,9 +127,15 @@ public static Response doJsonPut(String url, Map queryParams, Ma } } - public static Response doJsonDelete(String url, Map headers, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { + public static Response doJsonDelete(String url, Map headers, Map queryParams, JsonObject jsonInput) throws IOException, OAuthClientNotFoundException { try { - URL obj = new URL(url); + if (queryParams == null) { + queryParams = new HashMap<>(); + } + + URL obj = new URL(url + "?" + queryParams.entrySet().stream() + .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&"))); HttpURLConnection con = (HttpURLConnection) obj.openConnection(); con.setRequestMethod("DELETE"); con.setConnectTimeout(CONNECTION_TIMEOUT); diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index ba323cca2..0c73d6a33 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -37,12 +37,13 @@ import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.oauth.OAuthStorage; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.session.jwt.JWT.JWTException; import io.supertokens.utils.Utils; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.*; @@ -72,7 +73,7 @@ public static HttpRequestForOry.Response doOAuthProxyGET(Main main, AppIdentifie } if (clientIdToCheck != null) { - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -113,7 +114,7 @@ public static HttpRequestForOry.Response doOAuthProxyFormPOST(Main main, AppIden } if (clientIdToCheck != null) { - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -154,7 +155,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPOST(Main main, AppIden } if (clientIdToCheck != null) { - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -196,7 +197,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPUT(Main main, AppIdent } if (clientIdToCheck != null) { - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -228,7 +229,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonPUT(Main main, AppIdent return response; } - public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { + public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, Map queryParams, JsonObject jsonInput, Map headers) throws StorageQueryException, OAuthClientNotFoundException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException, OAuthAPIException { checkForOauthFeature(appIdentifier, main); OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -237,7 +238,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppId } if (clientIdToCheck != null) { - if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientIdToCheck)) { + if (!oauthStorage.doesClientIdExistForApp(appIdentifier, clientIdToCheck)) { throw new OAuthClientNotFoundException(); } } @@ -254,7 +255,7 @@ public static HttpRequestForOry.Response doOAuthProxyJsonDELETE(Main main, AppId } String fullUrl = baseURL + path; - HttpRequestForOry.Response response = HttpRequestForOry.doJsonDelete(fullUrl, headers, jsonInput); + HttpRequestForOry.Response response = HttpRequestForOry.doJsonDelete(fullUrl, queryParams, headers, jsonInput); // Response transformations response.jsonResponse = Transformations.transformJsonResponseFromHydra(main, appIdentifier, response.jsonResponse); @@ -283,31 +284,87 @@ private static void checkNonSuccessResponse(HttpRequestForOry.Response response) } } + public static String transformTokensInAuthRedirect(Main main, AppIdentifier appIdentifier, Storage storage, String url, String iss, JsonObject accessTokenUpdate, JsonObject idTokenUpdate, boolean useDynamicKey) { + if (url.indexOf('#') == -1) { + return url; + } + + try { + // Extract the part after '#' + String fragment = url.substring(url.indexOf('#') + 1); + + // Parse the fragment as query parameters + // Create a JsonObject from the params + JsonObject jsonBody = new JsonObject(); + for (String param : fragment.split("&")) { + String[] keyValue = param.split("=", 2); + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = java.net.URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8.toString()); + jsonBody.addProperty(key, value); + } + } + + // Transform the tokens + JsonObject transformedJson = transformTokens(main, appIdentifier, storage, jsonBody, iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); + + // Reconstruct the query params + StringBuilder newFragment = new StringBuilder(); + for (Map.Entry entry : transformedJson.entrySet()) { + if (newFragment.length() > 0) { + newFragment.append("&"); + } + String encodedValue = java.net.URLEncoder.encode(entry.getValue().getAsString(), StandardCharsets.UTF_8.toString()); + newFragment.append(entry.getKey()).append("=").append(encodedValue); + } + + // Reconstruct the URL + String baseUrl = url.substring(0, url.indexOf('#')); + return baseUrl + "#" + newFragment.toString(); + } catch (Exception e) { + // If any exception occurs, return the original URL + return url; + } + } + public static JsonObject transformTokens(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject jsonBody, String iss, JsonObject accessTokenUpdate, JsonObject idTokenUpdate, boolean useDynamicKey) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException, InvalidConfigException { + String atHash = null; + + if (jsonBody.has("refresh_token")) { + String refreshToken = jsonBody.get("refresh_token").getAsString(); + refreshToken = refreshToken.replace("ory_rt_", "st_rt_"); + jsonBody.addProperty("refresh_token", refreshToken); + } + if (jsonBody.has("access_token")) { String accessToken = jsonBody.get("access_token").getAsString(); - accessToken = OAuthToken.reSignToken(appIdentifier, main, accessToken, iss, accessTokenUpdate, OAuthToken.TokenType.ACCESS_TOKEN, useDynamicKey, 0); + accessToken = OAuthToken.reSignToken(appIdentifier, main, accessToken, iss, accessTokenUpdate, null, OAuthToken.TokenType.ACCESS_TOKEN, useDynamicKey, 0); jsonBody.addProperty("access_token", accessToken); + + // Compute at_hash as per OAuth 2.0 standard + // 1. Take the access token + // 2. Hash it with SHA-256 + // 3. Take the left-most half of the hash + // 4. Base64url encode it + byte[] accessTokenBytes = accessToken.getBytes(StandardCharsets.UTF_8); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(accessTokenBytes); + byte[] halfHash = Arrays.copyOf(hash, hash.length / 2); + atHash = Base64.getUrlEncoder().withoutPadding().encodeToString(halfHash); } if (jsonBody.has("id_token")) { String idToken = jsonBody.get("id_token").getAsString(); - idToken = OAuthToken.reSignToken(appIdentifier, main, idToken, iss, idTokenUpdate, OAuthToken.TokenType.ID_TOKEN, useDynamicKey, 0); + idToken = OAuthToken.reSignToken(appIdentifier, main, idToken, iss, idTokenUpdate, atHash, OAuthToken.TokenType.ID_TOKEN, useDynamicKey, 0); jsonBody.addProperty("id_token", idToken); } - if (jsonBody.has("refresh_token")) { - String refreshToken = jsonBody.get("refresh_token").getAsString(); - refreshToken = refreshToken.replace("ory_rt_", "st_rt_"); - jsonBody.addProperty("refresh_token", refreshToken); - } - return jsonBody; } - public static void addClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException { + public static void addOrUpdateClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId, boolean isClientCredentialsOnly) throws StorageQueryException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); - oauthStorage.addClientForApp(appIdentifier, clientId); + oauthStorage.addOrUpdateClientForApp(appIdentifier, clientId, isClientCredentialsOnly); } public static void removeClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException { @@ -365,16 +422,87 @@ private static JsonElement convertSnakeCaseToCamelCaseRecursively(JsonElement js } + public static void verifyAndUpdateIntrospectRefreshTokenPayload(Main main, AppIdentifier appIdentifier, + Storage storage, JsonObject payload, String refreshToken) throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException, InvalidConfigException, IOException { + + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + + if (!payload.get("active").getAsBoolean()) { + return; // refresh token is not active + } + + Transformations.transformExt(payload); + payload.remove("ext"); + + boolean isValid = !isTokenRevokedBasedOnPayload(oauthStorage, appIdentifier, payload); + + if (!isValid) { + payload.entrySet().clear(); + payload.addProperty("active", false); + + // // ideally we want to revoke the refresh token in hydra, but we can't since we don't have the client secret here + // refreshToken = refreshToken.replace("st_rt_", "ory_rt_"); + // Map formFields = new HashMap<>(); + // formFields.put("token", refreshToken); + + // try { + // doOAuthProxyFormPOST( + // main, appIdentifier, oauthStorage, + // clientId, // clientIdToCheck + // "/oauth2/revoke", // path + // false, // proxyToAdmin + // false, // camelToSnakeCaseConversion + // formFields, + // new HashMap<>()); + // } catch (OAuthAPIException | OAuthClientNotFoundException e) { + // // ignore + // } + } + } + + private static boolean isTokenRevokedBasedOnPayload(OAuthStorage oauthStorage, AppIdentifier appIdentifier, JsonObject payload) throws StorageQueryException { + long issuedAt = payload.get("iat").getAsLong(); + List targetTypes = new ArrayList<>(); + List targetValues = new ArrayList<>(); + + targetTypes.add("client_id"); + targetValues.add(payload.get("client_id").getAsString()); + + if (payload.has("jti")) { + targetTypes.add("jti"); + targetValues.add(payload.get("jti").getAsString()); + } + + if (payload.has("gid")) { + targetTypes.add("gid"); + targetValues.add(payload.get("gid").getAsString()); + } + + if (payload.has("sessionHandle")) { + targetTypes.add("session_handle"); + targetValues.add(payload.get("sessionHandle").getAsString()); + } + + return oauthStorage.isRevoked(appIdentifier, targetTypes.toArray(new String[0]), targetValues.toArray(new String[0]), issuedAt); + } + public static JsonObject introspectAccessToken(Main main, AppIdentifier appIdentifier, Storage storage, String token) throws StorageQueryException, StorageTransactionLogicException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException { try { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); JsonObject payload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, token); + if (payload.has("stt") && payload.get("stt").getAsInt() == OAuthToken.TokenType.ACCESS_TOKEN.getValue()) { - payload.addProperty("active", true); - payload.addProperty("token_type", "Bearer"); - payload.addProperty("token_use", "access_token"); - return payload; + boolean isValid = !isTokenRevokedBasedOnPayload(oauthStorage, appIdentifier, payload); + + if (isValid) { + payload.addProperty("active", true); + payload.addProperty("token_type", "Bearer"); + payload.addProperty("token_use", "access_token"); + + return payload; + } } // else fallback to active: false @@ -386,4 +514,82 @@ public static JsonObject introspectAccessToken(Main main, AppIdentifier appIdent result.addProperty("active", false); return result; } + + public static void revokeTokensForClientId(Main main, AppIdentifier appIdentifier, Storage storage, String clientId) throws StorageQueryException { + long exp = System.currentTimeMillis() / 1000 + 3600 * 24 * 183; // 6 month from now + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.revoke(appIdentifier, "client_id", clientId, exp); + } + + public static void revokeRefreshToken(Main main, AppIdentifier appIdentifier, Storage storage, String gid, long exp) throws StorageQueryException, NoSuchAlgorithmException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.revoke(appIdentifier, "gid", gid, exp); + } + + public static void revokeAccessToken(Main main, AppIdentifier appIdentifier, + Storage storage, String token) throws StorageQueryException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + try { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + JsonObject payload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, token); + + long exp = payload.get("exp").getAsLong(); + + if (payload.has("stt") && payload.get("stt").getAsInt() == OAuthToken.TokenType.ACCESS_TOKEN.getValue()) { + String jti = payload.get("jti").getAsString(); + oauthStorage.revoke(appIdentifier, "jti", jti, exp); + } + + } catch (TryRefreshTokenException e) { + // the token is already invalid or revoked, so ignore + } + } + + public static void revokeSessionHandle(Main main, AppIdentifier appIdentifier, Storage storage, + String sessionHandle) throws StorageQueryException { + long exp = System.currentTimeMillis() / 1000 + 3600 * 24 * 183; // 6 month from now + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + oauthStorage.revoke(appIdentifier, "session_handle", sessionHandle, exp); + } + + public static void verifyIdTokenHintClientIdAndUpdateQueryParamsForLogout(Main main, AppIdentifier appIdentifier, Storage storage, + Map queryParams) throws StorageQueryException, OAuthAPIException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + + String idTokenHint = queryParams.get("idTokenHint"); + String clientId = queryParams.get("clientId"); + + JsonObject idTokenPayload = null; + if (idTokenHint != null) { + queryParams.remove("idTokenHint"); + + try { + idTokenPayload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, idTokenHint); + } catch (TryRefreshTokenException e) { + // invalid id token + throw new OAuthAPIException("invalid_request", "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.", 400); + } + } + + if (idTokenPayload != null) { + if (!idTokenPayload.has("stt") || idTokenPayload.get("stt").getAsInt() != OAuthToken.TokenType.ID_TOKEN.getValue()) { + // Invalid id token + throw new OAuthAPIException("invalid_request", "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.", 400); + } + + String clientIdInIdTokenPayload = idTokenPayload.get("aud").getAsString(); + + if (clientId != null) { + if (!clientId.equals(clientIdInIdTokenPayload)) { + throw new OAuthAPIException("invalid_request", "The client_id in the id_token_hint does not match the client_id in the request.", 400); + } + } + + queryParams.put("clientId", clientIdInIdTokenPayload); + } + } + + public static void addM2MToken(Main main, AppIdentifier appIdentifier, Storage storage, String accessToken) throws StorageQueryException, TenantOrAppNotFoundException, TryRefreshTokenException, UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + JsonObject payload = OAuthToken.getPayloadFromJWTToken(appIdentifier, main, accessToken); + oauthStorage.addM2MToken(appIdentifier, payload.get("client_id").getAsString(), payload.get("iat").getAsLong(), payload.get("exp").getAsLong()); + } } diff --git a/src/main/java/io/supertokens/oauth/OAuthToken.java b/src/main/java/io/supertokens/oauth/OAuthToken.java index e04143c71..0d92b0b96 100644 --- a/src/main/java/io/supertokens/oauth/OAuthToken.java +++ b/src/main/java/io/supertokens/oauth/OAuthToken.java @@ -49,7 +49,7 @@ public int getValue() { private static Set NON_OVERRIDABLE_TOKEN_PROPS = Set.of( "kid", "typ", "alg", "aud", "iss", "iat", "exp", "nbf", "jti", "ext", - "sid", "rat", "at_hash", "rt_hash", + "sid", "rat", "at_hash", "gid", "client_id", "scp", "sub", "stt" ); @@ -95,18 +95,35 @@ public static JsonObject getPayloadFromJWTToken(AppIdentifier appIdentifier, return jwtInfo.payload; } - public static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, JsonObject payloadUpdate, TokenType tokenType, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, + public static String reSignToken(AppIdentifier appIdentifier, Main main, String token, String iss, JsonObject payloadUpdate, String atHash, TokenType tokenType, boolean useDynamicSigningKey, int retryCount) throws IOException, JWTException, InvalidKeyException, NoSuchAlgorithmException, StorageQueryException, StorageTransactionLogicException, UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, InvalidKeySpecException, JWTCreationException { JsonObject payload = JWT.getPayloadWithoutVerifying(token).payload; payload.addProperty("iss", iss); payload.addProperty("stt", tokenType.getValue()); + if (atHash != null) { + payload.addProperty("at_hash", atHash); + } if (tokenType == TokenType.ACCESS_TOKEN) { // we need to move rsub, tId and sessionHandle from ext to root Transformations.transformExt(payload); } + // This should only happen in the authorization code flow during the token exchange. (enforced on the api level) + // Other flows (including later calls using the refresh token) will have the payloadUpdate defined. + if (payloadUpdate == null) { + if (tokenType == TokenType.ACCESS_TOKEN) { + if (payload.has("ext") && payload.get("ext").isJsonObject()) { + payloadUpdate = payload.getAsJsonObject("ext").getAsJsonObject("initialPayload"); + payload.remove("ext"); + } + } else { + payloadUpdate = payload.getAsJsonObject("initialPayload"); + payload.remove("initialPayload"); + } + } + if (payloadUpdate != null) { for (Map.Entry entry : payloadUpdate.entrySet()) { if (!NON_OVERRIDABLE_TOKEN_PROPS.contains(entry.getKey())) { diff --git a/src/main/java/io/supertokens/oauth/Transformations.java b/src/main/java/io/supertokens/oauth/Transformations.java index 589a553cf..aa842470b 100644 --- a/src/main/java/io/supertokens/oauth/Transformations.java +++ b/src/main/java/io/supertokens/oauth/Transformations.java @@ -22,7 +22,37 @@ import io.supertokens.utils.Utils; public class Transformations { - private static Set EXT_PROPS = Set.of("rsub", "tId", "sessionHandle"); + private static Set EXT_PROPS = Set.of("iss", "rsub", "tId", "sessionHandle", "gid"); + + private static Set CLIENT_PROPS = Set.of( + "clientId", + "clientSecret", + "clientName", + "scope", + "redirectUris", + "postLogoutRedirectUris", + "authorizationCodeGrantAccessTokenLifespan", + "authorizationCodeGrantIdTokenLifespan", + "authorizationCodeGrantRefreshTokenLifespan", + "clientCredentialsGrantAccessTokenLifespan", + "implicitGrantAccessTokenLifespan", + "implicitGrantIdTokenLifespan", + "refreshTokenGrantAccessTokenLifespan", + "refreshTokenGrantIdTokenLifespan", + "refreshTokenGrantRefreshTokenLifespan", + "tokenEndpointAuthMethod", + "clientUri", + "allowedCorsOrigins", + "audience", + "grantTypes", + "responseTypes", + "logoUri", + "policyUri", + "tosUri", + "createdAt", + "updatedAt", + "metadata" + ); public static Map transformRequestHeadersForHydra(Map requestHeaders) { if (requestHeaders == null) { @@ -41,18 +71,19 @@ private static String transformQueryParamsInURLFromHydra(String redirectTo) { try { URL url = new URL(redirectTo); String query = url.getQuery(); - String[] queryParams = query.split("&"); - StringBuilder updatedQuery = new StringBuilder(); - for (String param : queryParams) { - String[] keyValue = param.split("="); - if (keyValue.length > 1 && keyValue[1].startsWith("ory_")) { - updatedQuery.append(keyValue[0]).append("=").append(keyValue[1].replaceFirst("ory_", "st_")).append("&"); - } else { - updatedQuery.append(param).append("&"); + if (query != null) { + String[] queryParams = query.split("&"); + StringBuilder updatedQuery = new StringBuilder(); + for (String param : queryParams) { + String[] keyValue = param.split("="); + if (keyValue.length > 1 && keyValue[1].startsWith("ory_")) { + updatedQuery.append(keyValue[0]).append("=").append(keyValue[1].replaceFirst("ory_", "st_")).append("&"); + } else { + updatedQuery.append(param).append("&"); + } } + redirectTo = redirectTo.replace("?" + query, "?" + updatedQuery.toString().trim()); } - redirectTo = url.getProtocol() + "://" + url.getHost() + ":" + url.getPort() + url.getPath() + "?" - + updatedQuery.toString().trim(); } catch (MalformedURLException e) { throw new IllegalStateException(e); } @@ -216,10 +247,20 @@ public static void transformExt(JsonObject payload) { ext.remove(prop); } } - - if (ext.entrySet().size() == 0) { - payload.remove("ext"); + } + } + + public static void applyClientPropsWhiteList(JsonObject payload) { + List propsToRemove = new ArrayList<>(); + + for (Map.Entry entry : payload.entrySet()) { + if (!CLIENT_PROPS.contains(entry.getKey())) { + propsToRemove.add(entry.getKey()); } } + + for (String prop : propsToRemove) { + payload.remove(prop); + } } } diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index da4c134aa..f283ca8b2 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -39,21 +39,7 @@ import io.supertokens.webserver.api.multitenancy.*; import io.supertokens.webserver.api.multitenancy.thirdparty.CreateOrUpdateThirdPartyConfigAPI; import io.supertokens.webserver.api.multitenancy.thirdparty.RemoveThirdPartyConfigAPI; -import io.supertokens.webserver.api.oauth.OAuthAuthAPI; -import io.supertokens.webserver.api.oauth.OAuthClientListAPI; -import io.supertokens.webserver.api.oauth.OAuthGetAuthConsentRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthGetAuthLoginRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthGetAuthLogoutRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthRejectAuthConsentRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthRejectAuthLoginRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthRejectAuthLogoutRequestAPI; -import io.supertokens.webserver.api.oauth.CreateUpdateOrGetOAuthClientAPI; -import io.supertokens.webserver.api.oauth.OAuthAcceptAuthConsentRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthAcceptAuthLoginRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthAcceptAuthLogoutRequestAPI; -import io.supertokens.webserver.api.oauth.OAuthTokenAPI; -import io.supertokens.webserver.api.oauth.OAuthTokenIntrospectAPI; -import io.supertokens.webserver.api.oauth.RemoveOAuthClientAPI; +import io.supertokens.webserver.api.oauth.*; import io.supertokens.webserver.api.passwordless.*; import io.supertokens.webserver.api.session.*; import io.supertokens.webserver.api.thirdparty.GetUsersByEmailAPI; @@ -299,6 +285,11 @@ private void setupRoutes() { addAPI(new OAuthRejectAuthLogoutRequestAPI(main)); addAPI(new OAuthTokenIntrospectAPI(main)); + addAPI(new RevokeOAuthTokenAPI(main)); + addAPI(new RevokeOAuthTokensAPI(main)); + addAPI(new RevokeOAuthSessionAPI(main)); + addAPI(new OAuthLogoutAPI(main)); + StandardContext context = tomcatReference.getContext(); Tomcat tomcat = tomcatReference.getTomcat(); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java index cfc811e8f..7203f0949 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/CreateUpdateOrGetOAuthClientAPI.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.UUID; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -28,6 +29,7 @@ import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.Transformations; import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.oauth.exceptions.OAuthClientNotFoundException; import io.supertokens.pluginInterface.RECIPE_ID; @@ -36,7 +38,6 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -70,6 +71,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO new HashMap<>() ); if (response != null) { + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { @@ -85,6 +88,12 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I input.addProperty("accessTokenStrategy", "jwt"); input.addProperty("skipConsent", true); input.addProperty("subjectType", "public"); + input.addProperty("clientId", "stcl_" + UUID.randomUUID()); + + boolean isClientCredentialsOnly = input.has("grantTypes") && + input.get("grantTypes").isJsonArray() && + input.get("grantTypes").getAsJsonArray().size() == 1 && + input.get("grantTypes").getAsJsonArray().get(0).getAsString().equals("client_credentials"); try { AppIdentifier appIdentifier = getAppIdentifier(req); @@ -107,12 +116,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String clientId = response.jsonResponse.getAsJsonObject().get("clientId").getAsString(); try { - OAuth.addClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId); + OAuth.addOrUpdateClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId, isClientCredentialsOnly); } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); - } catch (OAuth2ClientAlreadyExistsForAppException e) { - // ignore } + + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { @@ -124,6 +134,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { JsonObject input = InputParser.parseJsonObjectOrThrowError(req); String clientId = InputParser.parseStringOrThrowError(input, "clientId", false); + boolean isClientCredentialsOnly = input.has("grantTypes") && + input.get("grantTypes").isJsonArray() && + input.get("grantTypes").getAsJsonArray().size() == 1 && + input.get("grantTypes").getAsJsonArray().get(0).getAsString().equals("client_credentials"); // Apply existing client config on top of input try { @@ -165,6 +179,14 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO ); if (response != null) { + try { + OAuth.addOrUpdateClientId(main, getAppIdentifier(req), enforcePublicTenantAndGetPublicTenantStorage(req), clientId, isClientCredentialsOnly); + } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java index 3df0236d0..f9d28c9fc 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAcceptAuthConsentRequestAPI.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.util.HashMap; +import java.util.UUID; import com.google.gson.JsonObject; @@ -30,6 +31,36 @@ public String getPath() { @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String iss = InputParser.parseStringOrThrowError(input, "iss", false); + String tId = InputParser.parseStringOrThrowError(input, "tId", false); + String rsub = InputParser.parseStringOrThrowError(input, "rsub", false); + String sessionHandle = InputParser.parseStringOrThrowError(input, "sessionHandle", false); + JsonObject initialAccessTokenPayload = InputParser.parseJsonObjectOrThrowError(input, "initialAccessTokenPayload", false); + JsonObject initialIdTokenPayload = InputParser.parseJsonObjectOrThrowError(input, "initialIdTokenPayload", false); + + JsonObject accessToken = new JsonObject(); + accessToken.addProperty("iss", iss); + accessToken.addProperty("tId", tId); + accessToken.addProperty("rsub", rsub); + accessToken.addProperty("sessionHandle", sessionHandle); + accessToken.add("initialPayload", initialAccessTokenPayload); + + JsonObject idToken = new JsonObject(); + idToken.add("initialPayload", initialIdTokenPayload); + accessToken.addProperty("gid", UUID.randomUUID().toString()); + + // remove the above from input + input.remove("iss"); + input.remove("tId"); + input.remove("rsub"); + input.remove("sessionHandle"); + input.remove("initialAccessTokenPayload"); + input.remove("initialIdTokenPayload"); + + JsonObject session = new JsonObject(); + session.add("access_token", accessToken); + session.add("id_token", idToken); + input.add("session", session); try { HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonPUT( diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java index c7b604801..7ff44dcfc 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -23,7 +23,10 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; @@ -53,6 +56,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject params = InputParser.parseJsonObjectOrThrowError(input, "params", false); String cookies = InputParser.parseStringOrThrowError(input, "cookies", true); + // These optional stuff will be used in case of implicit flow + JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "access_token", true); + JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "id_token", true); + String iss = InputParser.parseStringOrThrowError(input, "iss", true); + Boolean useStaticKeyInput = InputParser.parseBooleanOrThrowError(input, "useStaticSigningKey", true); + boolean useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); + Map queryParams = params.entrySet().stream().collect(Collectors.toMap( Map.Entry::getKey, e -> e.getValue().getAsString() @@ -65,10 +75,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( main, req, resp, - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), + appIdentifier, + storage, queryParams.get("client_id"), // clientIdToCheck "/oauth2/auth", // proxyPath false, // proxyToAdmin @@ -83,8 +96,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } String redirectTo = response.headers.get("Location").get(0); + + redirectTo = OAuth.transformTokensInAuthRedirect(main, appIdentifier, storage, redirectTo, iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); List responseCookies = response.headers.get("Set-Cookie"); - + JsonObject finalResponse = new JsonObject(); finalResponse.addProperty("redirectTo", redirectTo); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java index fa40aec62..37f66ae6e 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthConsentRequestAPI.java @@ -6,6 +6,7 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.Transformations; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.WebserverAPI; @@ -40,6 +41,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO ); if (response != null) { + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java index d508369bc..cc3c06b2c 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLoginRequestAPI.java @@ -6,6 +6,7 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.Transformations; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.WebserverAPI; @@ -40,6 +41,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO ); if (response != null) { + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java index ad174654c..143e505b0 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthGetAuthLogoutRequestAPI.java @@ -6,6 +6,7 @@ import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.Transformations; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.WebserverAPI; @@ -40,6 +41,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO ); if (response != null) { + Transformations.applyClientPropsWhiteList(response.jsonResponse.getAsJsonObject().get("client").getAsJsonObject()); + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); super.sendJsonResponse(200, response.jsonResponse, resp); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java new file mode 100644 index 000000000..8e6626733 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthLogoutAPI.java @@ -0,0 +1,73 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.exceptions.OAuthAPIException; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class OAuthLogoutAPI extends WebserverAPI { + public OAuthLogoutAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/sessions/logout"; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + Map queryParams = OAuthProxyHelper.defaultGetQueryParamsFromRequest(req); + OAuth.verifyIdTokenHintClientIdAndUpdateQueryParamsForLogout(main, appIdentifier, storage, queryParams); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyGET( + main, req, resp, + appIdentifier, + storage, + queryParams.get("clientId"), // clientIdToCheck + "/oauth2/sessions/logout", // proxyPath + false, // proxyToAdmin + true, // camelToSnakeCaseConversion + queryParams, + new HashMap<>() // headers + ); + + if (response != null) { + JsonObject finalResponse = new JsonObject(); + String redirectTo = response.headers.get("Location").get(0); + + finalResponse.addProperty("status", "OK"); + finalResponse.addProperty("redirectTo", redirectTo); + + super.sendJsonResponse(200, finalResponse, resp); + } + + } catch (OAuthAPIException e) { + OAuthProxyHelper.handleOAuthAPIException(resp, e); + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | UnsupportedJWTSigningAlgorithmException | StorageTransactionLogicException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java index 3a30544d9..6764ae05d 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthProxyHelper.java @@ -92,9 +92,9 @@ public static HttpRequestForOry.Response proxyJsonPUT(Main main, HttpServletRequ public static HttpRequestForOry.Response proxyJsonDELETE(Main main, HttpServletRequest req, HttpServletResponse resp, AppIdentifier appIdentifier, Storage storage, String clientIdToCheck, String path, boolean proxyToAdmin, boolean camelToSnakeCaseConversion, - JsonObject jsonInput, Map headers) throws IOException, ServletException { + Map queryParams, JsonObject jsonInput, Map headers) throws IOException, ServletException { try { - return OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, jsonInput, headers); + return OAuth.doOAuthProxyJsonDELETE(main, appIdentifier, storage, clientIdToCheck, path, proxyToAdmin, camelToSnakeCaseConversion, queryParams, jsonInput, headers); } catch (OAuthClientNotFoundException e) { handleOAuthClientNotFoundException(resp); } catch (OAuthAPIException e) { @@ -131,7 +131,7 @@ private static void handleOAuthClientNotFoundException(HttpServletResponse resp) resp.getWriter().println(response.toString()); } - private static void handleOAuthAPIException(HttpServletResponse resp, OAuthAPIException e) throws IOException { + public static void handleOAuthAPIException(HttpServletResponse resp, OAuthAPIException e) throws IOException { JsonObject response = new JsonObject(); response.addProperty("status", "OAUTH_ERROR"); response.addProperty("error", e.error); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index 22a479c6a..843fc0245 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -19,10 +19,12 @@ import com.auth0.jwt.exceptions.JWTCreationException; import com.google.gson.*; import io.supertokens.Main; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.exceptions.OAuthAPIException; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; @@ -61,19 +63,71 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String iss = InputParser.parseStringOrThrowError(input, "iss", false); // input validation JsonObject bodyFromSDK = InputParser.parseJsonObjectOrThrowError(input, "inputBody", false); - JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "access_token", true); - JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "id_token", true); + String grantType = InputParser.parseStringOrThrowError(bodyFromSDK, "grant_type", false); + JsonObject accessTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "access_token", "authorization_code".equals(grantType)); + JsonObject idTokenUpdate = InputParser.parseJsonObjectOrThrowError(input, "id_token", "authorization_code".equals(grantType)); // useStaticKeyInput defaults to true, so we check if it has been explicitly set to false Boolean useStaticKeyInput = InputParser.parseBooleanOrThrowError(input, "useStaticSigningKey", true); boolean useDynamicKey = Boolean.FALSE.equals(useStaticKeyInput); + String authorizationHeader = InputParser.parseStringOrThrowError(input, "authorizationHeader", true); + + Map headers = new HashMap<>(); + if (authorizationHeader != null) { + headers.put("Authorization", authorizationHeader); + } + Map formFields = new HashMap<>(); for (Map.Entry entry : bodyFromSDK.entrySet()) { formFields.put(entry.getKey(), entry.getValue().getAsString()); } try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + // check if the refresh token is valid + if (grantType.equals("refresh_token")) { + String refreshToken = InputParser.parseStringOrThrowError(bodyFromSDK, "refresh_token", false); + + Map formFieldsForTokenIntrospect = new HashMap<>(); + formFieldsForTokenIntrospect.put("token", refreshToken); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/oauth2/introspect", // pathProxy + true, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFieldsForTokenIntrospect, + new HashMap<>() // headers + ); + + if (response == null) { + return; // proxy helper would have sent the error response + } + + JsonObject refreshTokenPayload = response.jsonResponse.getAsJsonObject(); + + try { + OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, refreshTokenPayload, refreshToken); + } catch (StorageQueryException | TenantOrAppNotFoundException | + FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + + if (!refreshTokenPayload.get("active").getAsBoolean()) { + // this is what ory would return for an invalid token + OAuthProxyHelper.handleOAuthAPIException(resp, new OAuthAPIException( + "token_inactive", "Token is inactive because it is malformed, expired or otherwise invalid. Token validation failed.", 401 + )); + return; + } + } + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( main, req, resp, getAppIdentifier(req), @@ -83,16 +137,22 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I false, // proxyToAdmin false, // camelToSnakeCaseConversion formFields, - new HashMap<>() // headers + headers // headers ); if (response != null) { try { - AppIdentifier appIdentifier = getAppIdentifier(req); - Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); - response.jsonResponse = OAuth.transformTokens(super.main, appIdentifier, storage, response.jsonResponse.getAsJsonObject(), iss, accessTokenUpdate, idTokenUpdate, useDynamicKey); - } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { + + if (grantType.equals("client_credentials")) { + try { + OAuth.addM2MToken(main, appIdentifier, storage, response.jsonResponse.getAsJsonObject().get("access_token").getAsString()); + } catch (Exception e) { + // ignore + } + } + + } catch (IOException | InvalidConfigException | TenantOrAppNotFoundException | StorageQueryException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | JWTCreationException | JWTException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { throw new ServletException(e); } diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java index 902e7de83..1bf281f8e 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java @@ -18,13 +18,14 @@ import com.google.gson.*; import io.supertokens.Main; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.HttpRequestForOry; import io.supertokens.oauth.OAuth; -import io.supertokens.oauth.Transformations; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; @@ -56,31 +57,35 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String token = InputParser.parseStringOrThrowError(input, "token", false); if (token.startsWith("st_rt_")) { - String iss = InputParser.parseStringOrThrowError(input, "iss", false); - Map formFields = new HashMap<>(); for (Map.Entry entry : input.entrySet()) { formFields.put(entry.getKey(), entry.getValue().getAsString()); } try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( main, req, resp, - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), + appIdentifier, + storage, null, // clientIdToCheck "/admin/oauth2/introspect", // pathProxy true, // proxyToAdmin false, // camelToSnakeCaseConversion - formFields, // formFields + formFields, new HashMap<>() // headers ); if (response != null) { JsonObject finalResponse = response.jsonResponse.getAsJsonObject(); - finalResponse.addProperty("iss", iss); - Transformations.transformExt(finalResponse); + try { + OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, finalResponse, token); + } catch (StorageQueryException | TenantOrAppNotFoundException | + FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } finalResponse.addProperty("status", "OK"); super.sendJsonResponse(200, finalResponse, resp); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java index 9e7f00c9b..6844eff81 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/RemoveOAuthClientAPI.java @@ -60,6 +60,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I "/admin/clients/" + clientId, // proxyPath true, // proxyToAdmin true, // camelToSnakeCaseConversion + new HashMap<>(), // queryParams new JsonObject(), // jsonBody new HashMap<>() // headers ); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthSessionAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthSessionAPI.java new file mode 100644 index 000000000..ef54c7ee6 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthSessionAPI.java @@ -0,0 +1,50 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class RevokeOAuthSessionAPI extends WebserverAPI { + public RevokeOAuthSessionAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/session/revoke"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + String sessionHandle = InputParser.parseStringOrThrowError(input, "sessionHandle", false); + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + OAuth.revokeSessionHandle(main, appIdentifier, storage, sessionHandle); + + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + super.sendJsonResponse(200, response, resp); + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java new file mode 100644 index 000000000..b71ddd6d6 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokenAPI.java @@ -0,0 +1,141 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class RevokeOAuthTokenAPI extends WebserverAPI { + public RevokeOAuthTokenAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/token/revoke"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + String token = InputParser.parseStringOrThrowError(input, "token", false); + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + if (token.startsWith("st_rt_")) { + String gid = null; + long exp = -1; + { + // introspect token to get gid + Map formFields = new HashMap<>(); + formFields.put("token", token); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/oauth2/introspect", // pathProxy + true, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFields, + new HashMap<>() // headers + ); + + if (response != null) { + JsonObject finalResponse = response.jsonResponse.getAsJsonObject(); + + try { + OAuth.verifyAndUpdateIntrospectRefreshTokenPayload(main, appIdentifier, storage, finalResponse, token); + if (finalResponse.get("active").getAsBoolean()) { + gid = finalResponse.get("gid").getAsString(); + exp = finalResponse.get("exp").getAsLong(); + } + } catch (StorageQueryException | TenantOrAppNotFoundException | + FeatureNotEnabledException | InvalidConfigException e) { + throw new ServletException(e); + } + } + } + + // revoking refresh token + String clientId = InputParser.parseStringOrThrowError(input, "client_id", false); + String clientSecret = InputParser.parseStringOrThrowError(input, "client_secret", true); + + String authorizationHeader = InputParser.parseStringOrThrowError(input, "authorizationHeader", true); + + Map headers = new HashMap<>(); + if (authorizationHeader != null) { + headers.put("Authorization", authorizationHeader); + } + + Map formFields = new HashMap<>(); + formFields.put("token", token); + formFields.put("client_id", clientId); + if (clientSecret != null) { + formFields.put("client_secret", clientSecret); + } + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyFormPOST( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + null, //clientIdToCheck + "/oauth2/revoke", // path + false, // proxyToAdmin + false, // camelToSnakeCaseConversion + formFields, // formFields + headers // headers + ); + + if (response != null) { + // Success response would mean that the clientId/secret has been validated + if (gid != null) { + try { + OAuth.revokeRefreshToken(main, appIdentifier, storage, gid, exp); + } catch (StorageQueryException | NoSuchAlgorithmException e) { + throw new ServletException(e); + } + } + + JsonObject finalResponse = new JsonObject(); + finalResponse.addProperty("status", "OK"); + super.sendJsonResponse(200, finalResponse, resp); + } + } else { + // revoking access token + OAuth.revokeAccessToken(main, appIdentifier, storage, token); + + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + super.sendJsonResponse(200, response, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | + UnsupportedJWTSigningAlgorithmException | StorageTransactionLogicException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java new file mode 100644 index 000000000..6aef1c3ef --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/RevokeOAuthTokensAPI.java @@ -0,0 +1,70 @@ +package io.supertokens.webserver.api.oauth; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.HttpRequestForOry; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class RevokeOAuthTokensAPI extends WebserverAPI { + + public RevokeOAuthTokensAPI(Main main){ + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/tokens/revoke"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String clientId = InputParser.parseStringOrThrowError(input, "client_id", false); + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + OAuth.revokeTokensForClientId(main, appIdentifier, storage, clientId); + + Map queryParams = new HashMap<>(); + queryParams.put("client_id", clientId); + + HttpRequestForOry.Response response = OAuthProxyHelper.proxyJsonDELETE( + main, req, resp, + appIdentifier, + storage, + null, // clientIdToCheck + "/admin/oauth2/tokens", // proxyPath + true, // proxyToAdmin + false, // camelToSnakeCaseConversion + queryParams, // queryParams + new JsonObject(), // jsonInput + new HashMap<>() // headers + ); + + if (response != null) { + response.jsonResponse.getAsJsonObject().addProperty("status", "OK"); + super.sendJsonResponse(200, response.jsonResponse, resp); + } + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException e) { + throw new ServletException(e); + } + } +} diff --git a/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java b/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java index bdc2e5ee1..bc27456e3 100644 --- a/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java +++ b/src/test/java/io/supertokens/test/oauth/api/OAuthAuthAPITest.java @@ -27,7 +27,6 @@ import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.oauth.OAuthAuthResponse; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; diff --git a/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java b/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java index c667ce72d..8b1169853 100644 --- a/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java +++ b/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java @@ -24,7 +24,6 @@ import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; -import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager;