diff --git a/config.yaml b/config.yaml index aed18f4cb..5f6a8f80f 100644 --- a/config.yaml +++ b/config.yaml @@ -167,3 +167,6 @@ core_config_version: 0 # (OPTIONAL | Default: oauth_provider_public_service_url) If specified, the core uses this URL to parse responses from # the oauth provider when the oauth provider's internal address differs from the known public provider address. # oauth_provider_url_configured_in_oauth_provider: + +# (Optional | Default: null) string value. The encryption key used for saving OAuth client secret on the database. +# oauth_client_secret_encryption_key: diff --git a/devConfig.yaml b/devConfig.yaml index acc443030..9557ada23 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -167,3 +167,6 @@ disable_telemetry: true # (OPTIONAL | Default: oauth_provider_public_service_url) If specified, the core uses this URL to parse responses from # the oauth provider when the oauth provider's internal address differs from the known public provider address. # oauth_provider_url_configured_in_oauth_provider: + +# (Optional | Default: null) string value. The encryption key used for saving OAuth client secret on the database. +# oauth_client_secret_encryption_key: diff --git a/src/test/java/io/supertokens/test/oauth/api/OAuthAPIHelper.java b/src/test/java/io/supertokens/test/oauth/api/OAuthAPIHelper.java new file mode 100644 index 000000000..2f8a5d823 --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/OAuthAPIHelper.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.oauth.api; + +import java.util.Map; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import com.google.gson.JsonObject; + +import io.supertokens.Main; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.webserver.WebserverAPI; + +public class OAuthAPIHelper { + // TODO WIP + public static JsonObject createClient(Main main, JsonObject createClientBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/clients", createClientBody, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + return response; + } + + public static JsonObject auth(Main main, JsonObject authBody) throws Exception { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + "http://localhost:3567/recipe/oauth/auth", authBody, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + return response; + } + + public static JsonObject acceptLoginRequest(Main main, Map queryParams, JsonObject acceptLoginChallengeBody) throws Exception { + String url = "http://localhost:3567/recipe/oauth/auth/requests/login/accept"; + if (queryParams != null && !queryParams.isEmpty()) { + StringBuilder queryString = new StringBuilder("?"); + for (Map.Entry entry : queryParams.entrySet()) { + if (queryString.length() > 1) { + queryString.append("&"); + } + String encodedValue = URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.toString()); + queryString.append(entry.getKey()).append("=").append(encodedValue); + } + url += queryString.toString(); + } + + JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(main, "", + url, acceptLoginChallengeBody, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + return response; + } + + public static JsonObject acceptConsentRequest(Main main, Map queryParams, JsonObject acceptConsentChallengeBody) throws Exception { + String url = "http://localhost:3567/recipe/oauth/auth/requests/consent/accept"; + if (queryParams != null && !queryParams.isEmpty()) { + StringBuilder queryString = new StringBuilder("?"); + for (Map.Entry entry : queryParams.entrySet()) { + if (queryString.length() > 1) { + queryString.append("&"); + } + String encodedValue = URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.toString()); + queryString.append(entry.getKey()).append("=").append(encodedValue); + } + url += queryString.toString(); + } + + JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(main, "", + url, acceptConsentChallengeBody, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + return response; + } +} diff --git a/src/test/java/io/supertokens/test/oauth/api/TestImplicitFlow.java b/src/test/java/io/supertokens/test/oauth/api/TestImplicitFlow.java new file mode 100644 index 000000000..60b188b1e --- /dev/null +++ b/src/test/java/io/supertokens/test/oauth/api/TestImplicitFlow.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.oauth.api; + +import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.totp.TotpLicenseTest; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import static org.junit.Assert.assertNotNull; + +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +public class TestImplicitFlow { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testImplicitGrantFlow() throws Exception { + String[] args = {"../"}; + + // TODO WIP + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("oauth_provider_public_service_url", "http://localhost:4444"); + Utils.setValueInConfig("oauth_provider_admin_service_url", "http://localhost:4445"); + Utils.setValueInConfig("oauth_provider_consent_login_base_url", "http://localhost:3001/auth"); + Utils.setValueInConfig("oauth_client_secret_encryption_key", "secret"); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH}); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject clientBody = new JsonObject(); + JsonArray grantTypes = new JsonArray(); + grantTypes.add(new JsonPrimitive("implicit")); + clientBody.add("grantTypes", grantTypes); + JsonArray responseTypes = new JsonArray(); + responseTypes.add(new JsonPrimitive("token")); + responseTypes.add(new JsonPrimitive("id_token")); + clientBody.add("responseTypes", responseTypes); + JsonArray redirectUris = new JsonArray(); + redirectUris.add(new JsonPrimitive("http://localhost.com:3000/auth/callback/supertokens")); + clientBody.add("redirectUris", redirectUris); + clientBody.addProperty("scope", "openid profile email"); + + JsonObject client = OAuthAPIHelper.createClient(process.getProcess(), clientBody); + + JsonObject authRequestBody = new JsonObject(); + JsonObject params = new JsonObject(); + params.addProperty("client_id", client.get("clientId").getAsString()); + params.addProperty("redirect_uri", "http://localhost.com:3000/auth/callback/supertokens"); + params.addProperty("response_type", "token"); + params.addProperty("scope", "openid profile email"); + params.addProperty("state", "test12345678"); + + authRequestBody.add("params", params); + + JsonObject authResponse = OAuthAPIHelper.auth(process.getProcess(), authRequestBody); + System.out.println("AuthResponse: " + authResponse); + String cookies = authResponse.get("cookies").getAsJsonArray().get(0).getAsString(); + cookies = cookies.split(";")[0]; + + String redirectTo = authResponse.get("redirectTo").getAsString(); + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001/auth"); + + URL url = new URL(redirectTo); + Map queryParams = splitQuery(url); + String loginChallenge = queryParams.get("login_challenge"); + + Map acceptLoginRequestParams = new HashMap<>(); + acceptLoginRequestParams.put("loginChallenge", loginChallenge); + + JsonObject acceptLoginRequestBody = new JsonObject(); + acceptLoginRequestBody.addProperty("subject", "someuserid"); + acceptLoginRequestBody.addProperty("remember", true); + acceptLoginRequestBody.addProperty("rememberFor", 3600); + + JsonObject acceptLoginRequestResponse = OAuthAPIHelper.acceptLoginRequest(process.getProcess(), acceptLoginRequestParams, acceptLoginRequestBody); + System.out.println("AcceptLoginRequest: " + acceptLoginRequestResponse); + + redirectTo = acceptLoginRequestResponse.get("redirectTo").getAsString(); + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001/auth"); + + url = new URL(redirectTo); + queryParams = splitQuery(url); + + params = new JsonObject(); + for (Map.Entry entry : queryParams.entrySet()) { + params.addProperty(entry.getKey(), entry.getValue()); + } + authRequestBody.add("params", params); + authRequestBody.addProperty("cookies", cookies); + + authResponse = OAuthAPIHelper.auth(process.getProcess(), authRequestBody); + System.out.println(authResponse); + + redirectTo = authResponse.get("redirectTo").getAsString(); + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001/auth"); + cookies = authResponse.get("cookies").getAsJsonArray().get(0).getAsString(); + cookies = cookies.split(";")[0]; + + url = new URL(redirectTo); + queryParams = splitQuery(url); + + String consentChallenge = queryParams.get("consent_challenge"); + + JsonObject acceptConsentRequestBody = new JsonObject(); + acceptConsentRequestBody.addProperty("remember", true); + acceptConsentRequestBody.addProperty("rememberFor", 3600); + acceptConsentRequestBody.addProperty("iss", "http://localhost:3001/auth"); + acceptConsentRequestBody.addProperty("tId", "public"); + acceptConsentRequestBody.addProperty("rsub", "someuser"); + acceptConsentRequestBody.addProperty("sessionHandle", "session-handle"); + acceptConsentRequestBody.add("initialAccessTokenPayload", new JsonObject()); + acceptConsentRequestBody.add("initialIdTokenPayload", new JsonObject()); + + queryParams = new HashMap<>(); + queryParams.put("consentChallenge", consentChallenge); + + JsonObject acceptConsentRequestResponse = OAuthAPIHelper.acceptConsentRequest(process.getProcess(), queryParams, acceptConsentRequestBody); + System.out.println("AcceptConsentRequest: " + acceptConsentRequestResponse); + + redirectTo = acceptConsentRequestResponse.get("redirectTo").getAsString(); + redirectTo = redirectTo.replace("{apiDomain}", "http://localhost:3001/auth"); + + url = new URL(redirectTo); + queryParams = splitQuery(url); + + params = new JsonObject(); + for (Map.Entry entry : queryParams.entrySet()) { + params.addProperty(entry.getKey(), entry.getValue()); + } + authRequestBody.add("params", params); + authRequestBody.addProperty("cookies", cookies); + + authResponse = OAuthAPIHelper.auth(process.getProcess(), authRequestBody); + System.out.println(authResponse); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + // Helper method to split query parameters + private static Map splitQuery(URL url) throws UnsupportedEncodingException { + Map queryPairs = new LinkedHashMap<>(); + String query = url.getQuery(); + String[] pairs = query.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + queryPairs.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), + URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); + } + return queryPairs; + } +}