Skip to content

Commit

Permalink
feat: oauth2 auth API - WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
tamassoltesz committed Jul 29, 2024
1 parent 1bc72f9 commit 8a33068
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 11 deletions.
8 changes: 7 additions & 1 deletion src/main/java/io/supertokens/inmemorydb/Start.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -3007,4 +3008,9 @@ public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(A
throw new StorageQueryException(e);
}
}

@Override
public boolean doesClientIdExistForThisApp(AppIdentifier appIdentifier, String clientId) {
return false;
}
}
68 changes: 58 additions & 10 deletions src/main/java/io/supertokens/oauth/OAuth.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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<String, String> queryParamsForHydra = constructHydraRequestParams(clientId, redirectURI, responseType, scope, state);
Map<String, String> 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<String, String> constructHydraRequestParams(String clientId,
String redirectURI, String responseType, String scope, String state) {
Map<String, String> 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;
}
}
21 changes: 21 additions & 0 deletions src/main/java/io/supertokens/oauth/exceptions/OAuthException.java
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions src/main/java/io/supertokens/webserver/Webserver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down
79 changes: 79 additions & 0 deletions src/main/java/io/supertokens/webserver/api/oauth/OAuthAPI.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

0 comments on commit 8a33068

Please sign in to comment.