Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Oauth - WIP #1018

Merged
merged 30 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1bc72f9
fix/change annotations for configs
tamassoltesz Jul 29, 2024
8a33068
feat: oauth2 auth API - WIP
tamassoltesz Jul 29, 2024
d358a33
fix: hidefromdashboard to oauth_provider service url configs
tamassoltesz Jul 29, 2024
282b889
feat: OAuthAPI input parsing, basic flow
tamassoltesz Jul 29, 2024
4076bf8
feat: first test in progress
tamassoltesz Jul 29, 2024
b9c408a
fix: review fixes
tamassoltesz Jul 30, 2024
24523bd
feat: tables for oauth in sqlite
tamassoltesz Jul 30, 2024
c969ef1
fix: remove unnecessary tables
tamassoltesz Jul 30, 2024
c03d6c4
fix: store only the necessary data in the client table
tamassoltesz Jul 30, 2024
c740082
feat: oauth client - app exists check in db, a few tests
tamassoltesz Jul 30, 2024
c4c6438
fix: review fixes
tamassoltesz Jul 31, 2024
b84e901
fix: review fixes
tamassoltesz Jul 31, 2024
fc08d8c
feat: new configs for handling errors from hydra
tamassoltesz Jul 31, 2024
31164ef
feat: using the new configs for oauth provider
tamassoltesz Jul 31, 2024
75ceba0
fix: CHANGELOG
tamassoltesz Jul 31, 2024
b6bb95f
fix: tests for the new Util method
tamassoltesz Jul 31, 2024
df6cb85
fix: changelog changes
tamassoltesz Jul 31, 2024
5f6c78e
fix: changelog migration section fix
tamassoltesz Jul 31, 2024
85b45f8
fix: fixing repeated header handling in HttpRequest#sendGETRequestWit…
tamassoltesz Jul 31, 2024
bb09c56
fix: more tests for oauth auth
tamassoltesz Jul 31, 2024
1e5015c
fix: review fix - more checks for the oauth config validity
tamassoltesz Aug 1, 2024
955b106
fix: review fix - throwing expection if there is no location header i…
tamassoltesz Aug 1, 2024
2893b19
fix: review fix - renamed exception
tamassoltesz Aug 1, 2024
d14267f
feat: oauth2 register client API
tamassoltesz Aug 1, 2024
f5db9b2
feat: oauth2 get clients API
tamassoltesz Aug 1, 2024
3e3e5cf
feat: OAuth2 DELETE clients API
tamassoltesz Aug 1, 2024
2ac627e
fix: following the already existing response pattern with the oauth2 …
tamassoltesz Aug 2, 2024
7044395
fix: renaming exception to be more expressive
tamassoltesz Aug 2, 2024
a58f329
fix: review fixes
tamassoltesz Aug 5, 2024
ce52518
fix: using BadRequestException instead of custom format for hydra inv…
tamassoltesz Aug 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/main/java/io/supertokens/config/CoreConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -275,15 +275,16 @@ public class CoreConfig {
" address.")
private String ip_deny_regex = null;

@ConfigYamlOnly
@NotConflictingInApp
@JsonProperty
@HideFromDashboard
@ConfigDescription(
"If specified, the core uses this URL to connect to the OAuth provider public service.")
private String oauth_provider_public_service_url = null;

@ConfigYamlOnly
@NotConflictingInApp
@JsonProperty
@HideFromDashboard
@ConfigDescription(
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
"If specified, the core uses this URL to connect to the OAuth provider admin service.")
private String oauth_provider_admin_service_url = null;
Expand Down
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 {
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved

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();
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved

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?
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved

rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
if(!responseHeaders.isEmpty() && responseHeaders.containsKey(LOCATION_HEADER_NAME)) {
String locationHeaderValue = responseHeaders.get(LOCATION_HEADER_NAME);

if (locationHeaderValue.equals(publicOAuthProviderServiceUrl)){
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
throw new OAuthException();
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
}

if (locationHeaderValue.equals("localhost:3000")) {
redirectTo = Multitenancy.getAPIDomain(storage, appIdentifier);
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
} else {
redirectTo = locationHeaderValue;
}
}
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
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,
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
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{
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
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
98 changes: 98 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,98 @@
/*
* 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.Storage;
import io.supertokens.pluginInterface.exceptions.InvalidConfigException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.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 org.apache.catalina.Store;

import java.io.IOException;
import java.io.Serial;

public class OAuthAPI extends WebserverAPI {
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
@Serial
private static final long serialVersionUID = -8734479943734920904L;

public OAuthAPI(Main main) {
super(main, RECIPE_ID.OAUTH.toString());
}

@Override
public String getPath() {
return "oauth/auth";
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
// TODO Work in progress!

JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
String clientId = InputParser.parseStringOrThrowError(input, "clientId", false);
String redirectUri = InputParser.parseStringOrThrowError(input, "redirectUri", false);
String responseType = InputParser.parseStringOrThrowError(input, "responseType", false);
String scope = InputParser.parseStringOrThrowError(input, "scope", false);
String state = InputParser.parseStringOrThrowError(input, "state", false);

try {
AppIdentifier appIdentifier = getAppIdentifier(req);
Storage storage = getTenantStorage(req);
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved

OAuthAuthResponse authResponse = OAuth.getAuthorizationUrl(super.main, appIdentifier, storage,
clientId, redirectUri, responseType, scope, state);
JsonObject response = new JsonObject();
response.addProperty("redirectTo", authResponse.redirectTo);

if (authResponse.cookies != null) {
Gson gson = new Gson();
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
String cookiesAsJson = gson.toJson(authResponse.cookies);
response.addProperty("cookies", cookiesAsJson);
}

super.sendJsonResponse(200, response, resp);

} catch (OAuthException e) {

JsonObject errorResponse = new JsonObject();
//TODO what is a good content here?
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved

super.sendJsonResponse(400, errorResponse, resp);

} catch (TenantOrAppNotFoundException | InvalidConfigException | HttpResponseException |
StorageQueryException e) {
throw new ServletException(e);
}
}
}
102 changes: 102 additions & 0 deletions src/test/java/io/supertokens/test/oauth/api/OAuthAPITest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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.FeatureFlagTestContent;
import io.supertokens.httpRequest.HttpResponseException;
import io.supertokens.oauth.OAuth;
import io.supertokens.oauth.exceptions.OAuthException;
import io.supertokens.pluginInterface.STORAGE_TYPE;
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.pluginInterface.oauth.OAuthStorage;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.test.TestingProcessManager;
import io.supertokens.test.Utils;
import io.supertokens.webserver.InputParser;
import org.junit.*;
import org.junit.rules.TestRule;

import java.io.IOException;

import static org.junit.Assert.assertNotNull;

public class OAuthAPITest {
TestingProcessManager.TestingProcess process;

@Rule
public TestRule watchman = Utils.getOnFailure();

@AfterClass
public static void afterTesting() {
Utils.afterTesting();
}

@Before
public void beforeEach() throws InterruptedException {
Utils.reset();
String[] args = {"../"};

this.process = TestingProcessManager.start(args);
FeatureFlagTestContent.getInstance(process.getProcess())
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.OAUTH});
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));

if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { //TODO check if this is true here also
return;
}
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
}

@After
public void afterEach() throws InterruptedException {
process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}

@Test
public void test()
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
throws StorageQueryException, OAuthException, HttpResponseException, TenantOrAppNotFoundException,
InvalidConfigException, IOException {

String clientId = "a685663d-1b5d-4a70-b7f7-025ff2e2d7a4";
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";

OAuthStorage oAuthStorage = (OAuthStorage) StorageLayer.getStorage(process.getProcess());

OAuthAuthResponse response = OAuth.getAuthorizationUrl(process.getProcess(), null, oAuthStorage, clientId, redirectUri, responseType, scope, state);

System.out.println(response);
System.out.println(response.redirectTo);
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved

for(String cooke : response.cookies){
System.out.println(cooke);
}

assertNotNull(response);
assertNotNull(response.redirectTo);
assertNotNull(response.cookies);
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
}

}
Loading