diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 4398107bb..c4b199d8c 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -55,6 +55,7 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.multitenancy.sqlStorage.MultitenancySQLStorage; +import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; import io.supertokens.pluginInterface.passwordless.exception.*; @@ -102,7 +103,7 @@ public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, TOTPSQLStorage, ActiveUsersStorage, - ActiveUsersSQLStorage, DashboardSQLStorage, AuthRecipeSQLStorage { + ActiveUsersSQLStorage, DashboardSQLStorage, AuthRecipeSQLStorage, OAuthStorage { private static final Object appenderLock = new Object(); private static final String APP_ID_KEY_NAME = "app_id"; @@ -3007,4 +3008,9 @@ public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(A throw new StorageQueryException(e); } } + + @Override + public boolean doesClientIdExistForThisApp(AppIdentifier appIdentifier, String clientId) { + return false; + } } diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index 075ff1b96..63c583d34 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -16,40 +16,88 @@ package io.supertokens.oauth; +import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.config.Config; +import io.supertokens.httpRequest.HttpRequest; +import io.supertokens.httpRequest.HttpResponseException; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.oauth.exceptions.OAuthException; 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.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.oauth.OAuthAuthResponse; import io.supertokens.pluginInterface.oauth.OAuthStorage; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + public class OAuth { - public static void getAuthorizationUrl(Main main, AppIdentifier appIdentifier, Storage storage, String clientId, - String redirectURI, String responseType, String scope, String state) - throws InvalidConfigException { + private static final String LOCATION_HEADER_NAME = "Location"; + private static final String COOKIES_HEADER_NAME = "Set-Cookie"; + + public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier appIdentifier, Storage storage, String clientId, + String redirectURI, String responseType, String scope, String state) + throws InvalidConfigException, HttpResponseException, IOException, OAuthException, StorageQueryException, + TenantOrAppNotFoundException { // TODO: // - validate that client_id is present for this tenant - // - call hydra - // - if location header is: - // - localhost:3000, then we redirect to apiDomain - // - public url for hydra, then we throw a 400 error with the right json - // - else we redirect back to the client OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); String redirectTo = null; + List cookies = null; + + String publicOAuthProviderServiceUrl = Config.getBaseConfig(main).getOAuthProviderPublicServiceUrl(); if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { - redirectTo = Config.getBaseConfig(main).getOAuthProviderPublicServiceUrl() + + redirectTo = publicOAuthProviderServiceUrl + "/oauth2/fallbacks/error?error=invalid_client&error_description=Client+authentication+failed+%28e" + ".g.%2C+unknown+client%2C+no+client+authentication+included%2C+or+unsupported+authentication" + "+method%29.+The+requested+OAuth+2.0+Client+does+not+exist."; } else { // we query hydra + Map queryParamsForHydra = constructHydraRequestParams(clientId, redirectURI, responseType, scope, state); + Map responseHeaders = new HashMap<>(); + HttpRequest.sendGETRequestWithResponseHeaders(main, null, Config.getBaseConfig(main).getOAuthProviderPublicServiceUrl(), queryParamsForHydra, 20, 400, null, responseHeaders); // TODO is there some kind of config for the timeouts? + + if(null != responseHeaders && responseHeaders.keySet().contains(LOCATION_HEADER_NAME)) { + String locationHeaderValue = responseHeaders.get(LOCATION_HEADER_NAME); + + if (locationHeaderValue.equals(publicOAuthProviderServiceUrl)){ + throw new OAuthException(); + } + + if (locationHeaderValue.equals("localhost:3000")) { + redirectTo = Multitenancy.getAPIDomain(storage, appIdentifier); + } else { + redirectTo = locationHeaderValue; + } + } + if(responseHeaders.containsKey(COOKIES_HEADER_NAME)){ + String allCookies = responseHeaders.get(COOKIES_HEADER_NAME); + cookies = Arrays.asList(allCookies.split(";")); + } } - // TODO: parse url resposne and send appropriate reply from this API. + return new OAuthAuthResponse(redirectTo, cookies); + } + + private static Map constructHydraRequestParams(String clientId, + String redirectURI, String responseType, String scope, String state) { + Map queryParamsForHydra = new HashMap<>(); + queryParamsForHydra.put("clientId", clientId); + queryParamsForHydra.put("redirectURI", redirectURI); + queryParamsForHydra.put("scope", scope); + queryParamsForHydra.put("responseType", responseType); + queryParamsForHydra.put("state", state); + return queryParamsForHydra; } } diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthException.java new file mode 100644 index 000000000..2cc528d43 --- /dev/null +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthException.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.oauth.exceptions; + +public class OAuthException extends Exception{ + private static final long serialVersionUID = 1836718299845759897L; +} diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index 2861588fd..b5516db69 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -39,6 +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.OAuthAPI; import io.supertokens.webserver.api.passwordless.*; import io.supertokens.webserver.api.session.*; import io.supertokens.webserver.api.thirdparty.GetUsersByEmailAPI; @@ -267,6 +268,8 @@ private void setupRoutes() { addAPI(new RequestStatsAPI(main)); addAPI(new GetTenantCoreConfigForDashboardAPI(main)); + addAPI(new OAuthAPI(main)); + StandardContext context = tomcatReference.getContext(); Tomcat tomcat = tomcatReference.getTomcat(); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAPI.java new file mode 100644 index 000000000..b4919e9c4 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAPI.java @@ -0,0 +1,79 @@ +/* + * 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.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.httpRequest.HttpResponseException; +import io.supertokens.oauth.OAuth; +import io.supertokens.oauth.exceptions.OAuthException; +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.OAuthAuthResponse; +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; + +public class OAuthAPI extends WebserverAPI { + @Serial + private static final long serialVersionUID = -8734479943734920904L; + + public OAuthAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "oauth/auth"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + // TODO Work in progress! + try { + OAuthAuthResponse authResponse = OAuth.getAuthorizationUrl(null, null,null, "a685663d-1b5d-4a70-b7f7-025ff2e2d7a4", "http://localhost.com:3031/auth/callback/ory", "code", "profile", "%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 response = new JsonObject(); + response.addProperty("redirectTo", authResponse.redirectTo); + + if(authResponse.cookies != null) { + Gson gson = new Gson(); + String cookiesAsJson = gson.toJson(authResponse.cookies); + response.addProperty("cookies", cookiesAsJson); + } + + } catch (InvalidConfigException e) { + throw new RuntimeException(e); + } catch (HttpResponseException e) { + throw new RuntimeException(e); + } catch (OAuthException e) { + throw new RuntimeException(e); + } catch (StorageQueryException e) { + throw new RuntimeException(e); + } catch (TenantOrAppNotFoundException e) { + throw new RuntimeException(e); + } + } +}