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 all 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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Added new feature in license key: `OAUTH`
- Adds new core config:
- `oauth_provider_public_service_url`
- `oauth_provider_admin_service_url`
- `oauth_provider_consent_login_base_url`
- `oauth_provider_url_configured_in_hydra`
- Adds POST `/recipe/oauth/auth` for OAuth2 auth flow support
- Adds POST `/recipe/oauth/clients` for OAuth2 client registration
- Adds GET `/recipe/oauth/clients?clientId=example_id` for loading OAuth2 client
- Adds DELETE `/recipe/oauth/clients` for deleting OAuth2 Clients
- Creates new table `oauth_clients`

### Migration
TODO: after plugin support

## [9.1.1] -2024-07-24

Expand Down
8 changes: 8 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,11 @@ core_config_version: 0
# (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:

# (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:

# (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:
11 changes: 10 additions & 1 deletion devConfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,13 @@ disable_telemetry: true

# (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:


# (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:

# (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:
101 changes: 98 additions & 3 deletions src/main/java/io/supertokens/config/CoreConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.regex.PatternSyntaxException;

Expand All @@ -63,7 +65,9 @@ public class CoreConfig {
"ip_allow_regex",
"ip_deny_regex",
"oauth_provider_public_service_url",
"oauth_provider_admin_service_url"
"oauth_provider_admin_service_url",
"oauth_provider_consent_login_base_url",
"oauth_provider_url_configured_in_hydra"
};

@IgnoreForAnnotationCheck
Expand Down Expand Up @@ -275,19 +279,36 @@ 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;

@NotConflictingInApp
@JsonProperty
@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";

@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;



@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
Expand Down Expand Up @@ -347,6 +368,20 @@ public String getOAuthProviderAdminServiceUrl() throws InvalidConfigException {
return oauth_provider_admin_service_url;
}

public String getOauthProviderConsentLoginBaseUrl() throws InvalidConfigException {
if(oauth_provider_consent_login_base_url == null){
throw new InvalidConfigException("oauth_provider_consent_login_base_url is not set");
}
return oauth_provider_consent_login_base_url;
}

public String getOauthProviderUrlConfiguredInHydra() throws InvalidConfigException {
if(oauth_provider_url_configured_in_hydra == null) {
throw new InvalidConfigException("oauth_provider_url_configured_in_hydra is not set");
}
return oauth_provider_url_configured_in_hydra;
}

public String getIpAllowRegex() {
return ip_allow_regex;
}
Expand Down Expand Up @@ -833,6 +868,46 @@ void normalizeAndValidate(Main main, boolean includeConfigFilePath) throws Inval
}
}

if(oauth_provider_public_service_url != null) {
try {
URL url = new URL(oauth_provider_public_service_url);
} catch (MalformedURLException malformedURLException){
throw new InvalidConfigException("oauth_provider_public_service_url is not a valid URL");
}
}

if(oauth_provider_admin_service_url != null) {
try {
URL url = new URL(oauth_provider_admin_service_url);
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
} catch (MalformedURLException malformedURLException){
throw new InvalidConfigException("oauth_provider_admin_service_url is not a valid URL");
}
}

if(oauth_provider_consent_login_base_url != null) {
try {
URL url = new URL(oauth_provider_consent_login_base_url);
} catch (MalformedURLException malformedURLException){
throw new InvalidConfigException("oauth_provider_consent_login_base_url is not a valid URL");
}
}


if(oauth_provider_url_configured_in_hydra == null) {
oauth_provider_url_configured_in_hydra = oauth_provider_public_service_url;
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
} else {
try {
URL url = new URL(oauth_provider_url_configured_in_hydra);
} catch (MalformedURLException malformedURLException){
throw new InvalidConfigException("oauth_provider_url_configured_in_hydra is not a valid URL");
}
}

rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
List<String> configsTogetherSet = Arrays.asList(oauth_provider_public_service_url, oauth_provider_admin_service_url, oauth_provider_consent_login_base_url);
if(isAnySet(configsTogetherSet) && !isAllSet(configsTogetherSet)) {
throw new InvalidConfigException("If any of the following is set, all of them has to be set: oauth_provider_public_service_url, oauth_provider_admin_service_url, oauth_provider_consent_login_base_url");
}

isNormalizedAndValid = true;
}

Expand Down Expand Up @@ -963,4 +1038,24 @@ void assertThatConfigFromSameAppIdAreNotConflicting(CoreConfig other) throws Inv
public String getMaxCDIVersion() {
return this.supertokens_max_cdi_version;
}

private boolean isAnySet(List<String> configs){
for (String config : configs){
if(config!=null){
return true;
}
}
return false;
}

private boolean isAllSet(List<String> configs) {
boolean foundNotSet = false;
for(String config: configs){
if(config == null){
foundNotSet = true;
break;
}
}
return !foundNotSet;
}
}
7 changes: 4 additions & 3 deletions src/main/java/io/supertokens/httpRequest/HttpRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

public class HttpRequest {
Expand Down Expand Up @@ -126,7 +127,7 @@ public static <T> T sendGETRequest(Main main, String requestID, String url, Map<
public static <T> T sendGETRequestWithResponseHeaders(Main main, String requestID, String url,
Map<String, String> params,
int connectionTimeoutMS, int readTimeoutMS, Integer version,
Map<String, String> responseHeaders)
Map<String, List<String>> responseHeaders, boolean followRedirects)
throws IOException, HttpResponseException {
StringBuilder paramBuilder = new StringBuilder();

Expand All @@ -152,12 +153,12 @@ public static <T> T sendGETRequestWithResponseHeaders(Main main, String requestI
if (version != null) {
con.setRequestProperty("api-version", version + "");
}

con.setInstanceFollowRedirects(followRedirects);
int responseCode = con.getResponseCode();

con.getHeaderFields().forEach((key, value) -> {
if (key != null) {
responseHeaders.put(key, value.get(0));
responseHeaders.put(key, value);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ public class HttpResponseException extends Exception {
private static final long serialVersionUID = 1L;

public final int statusCode;
public final String rawMessage;

HttpResponseException(int statusCode, String message) {
super("Http error. Status Code: " + statusCode + ". Message: " + message);
this.statusCode = statusCode;
this.rawMessage = message;
}
}
41 changes: 40 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,8 @@
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;
import io.supertokens.pluginInterface.passwordless.exception.*;
Expand Down Expand Up @@ -102,7 +104,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, OAuthSQLStorage {

private static final Object appenderLock = new Object();
private static final String APP_ID_KEY_NAME = "app_id";
Expand Down Expand Up @@ -3007,4 +3009,41 @@ public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(A
throw new StorageQueryException(e);
}
}

@Override
public boolean doesClientIdExistForThisApp(AppIdentifier appIdentifier, String clientId)
throws StorageQueryException {
try {
return OAuthQueries.isClientIdForAppId(this, clientId, appIdentifier);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
public void addClientForApp(AppIdentifier appIdentifier, String clientId)
throws StorageQueryException, OAuth2ClientAlreadyExistsForAppException {
try {
OAuthQueries.insertClientIdForAppId(this, clientId, appIdentifier);
} 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);
}
}

@Override
public boolean removeAppClientAssociation(AppIdentifier appIdentifier, String clientId) throws StorageQueryException {
try {
return OAuthQueries.deleteClientIdForAppId(this, clientId, appIdentifier);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,6 @@ public String getDashboardUsersTable() {
public String getDashboardSessionsTable() {
return "dashboard_user_sessions";
}

public String getOAuthClientTable(){ return "oauth_clients"; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,10 @@ 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())) {
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, OAuthQueries.getQueryToCreateOAuthClientTable(start), NO_OP_SETTER);
}
}


Expand Down
75 changes: 75 additions & 0 deletions src/main/java/io/supertokens/inmemorydb/queries/OAuthQueries.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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.inmemorydb.queries;

import io.supertokens.inmemorydb.Start;
import io.supertokens.inmemorydb.config.Config;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;

import java.sql.ResultSet;
import java.sql.SQLException;

import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;

public class OAuthQueries {

public static String getQueryToCreateOAuthClientTable(Start start) {
String oAuth2ClientTable = Config.getConfig(start).getOAuthClientTable();
// @formatter:off
return "CREATE TABLE IF NOT EXISTS " + oAuth2ClientTable + " ("
+ "app_id VARCHAR(64),"
+ "client_id VARCHAR(128) 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 boolean isClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier)
throws SQLException, StorageQueryException {
String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthClientTable() +
" WHERE client_id = ? AND app_id = ?";

return execute(start, QUERY, pst -> {
pst.setString(1, clientId);
pst.setString(2, appIdentifier.getAppId());
}, ResultSet::next);
}

public static void insertClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier)
throws SQLException, StorageQueryException {
String INSERT = "INSERT INTO " + Config.getConfig(start).getOAuthClientTable()
+ "(app_id, client_id) VALUES(?, ?)";
update(start, INSERT, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, clientId);
});
}

public static boolean deleteClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier)
throws SQLException, StorageQueryException {
String DELETE = "DELETE FROM " + Config.getConfig(start).getOAuthClientTable()
+ " WHERE app_id = ? AND client_id = ?";
int numberOfRow = update(start, DELETE, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, clientId);
});
return numberOfRow > 0;
}

}
Loading
Loading