Skip to content

Commit

Permalink
fix: logout apis (#1047)
Browse files Browse the repository at this point in the history
* fix: logout apis

* fix: session revoke in logout

* fix: tests

* feat: update logout logic to make BE integration easier

* refactor: minor fixes for consistency

---------

Co-authored-by: Mihaly Lengyel <[email protected]>
  • Loading branch information
sattvikc and porcellus authored Sep 27, 2024
1 parent 2ea8d10 commit 6f225c5
Show file tree
Hide file tree
Showing 24 changed files with 385 additions and 197 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `oauth_provider_public_service_url`
- `oauth_provider_admin_service_url`
- `oauth_provider_consent_login_base_url`
- `oauth_provider_url_configured_in_hydra`
- `oauth_provider_url_configured_in_oauth_provider`
- 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
Expand Down
10 changes: 5 additions & 5 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,17 @@ core_config_version: 0
# supertokens_saas_load_only_cud:

# (OPTIONAL | Default: null) string value. If specified, the core uses this URL to connect to the OAuth provider
# public service.
# public service.
# oauth_provider_public_service_url:

# (OPTIONAL | Default: null) string value. If specified, the core uses this URL to connect to the OAuth provider admin
# service.
# service.
# oauth_provider_admin_service_url:

# (OPTIONAL | Default: null) 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:
# (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:
16 changes: 8 additions & 8 deletions devConfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,17 @@ disable_telemetry: true
# supertokens_saas_load_only_cud:

# (OPTIONAL | Default: null) string value. If specified, the core uses this URL to connect to the OAuth provider
# public service.
oauth_provider_public_service_url: http://localhost:4444
# public service.
# oauth_provider_public_service_url:

# (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: http://localhost:4445
# service.
# oauth_provider_admin_service_url:

# (OPTIONAL | Default: null) string value. If specified, the core uses this URL replace the default
# consent and login URLs to {apiDomain}.
oauth_provider_consent_login_base_url: http://localhost:4001/auth
# 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:
# (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:
4 changes: 2 additions & 2 deletions src/main/java/io/supertokens/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import io.supertokens.config.Config;
import io.supertokens.config.CoreConfig;
import io.supertokens.cronjobs.Cronjobs;
import io.supertokens.cronjobs.cleanupOAuthRevokeList.CleanupOAuthRevokeList;
import io.supertokens.cronjobs.cleanupOAuthRevokeListAndChallenges.CleanupOAuthRevokeListAndChallenges;
import io.supertokens.cronjobs.deleteExpiredAccessTokenSigningKeys.DeleteExpiredAccessTokenSigningKeys;
import io.supertokens.cronjobs.deleteExpiredDashboardSessions.DeleteExpiredDashboardSessions;
import io.supertokens.cronjobs.deleteExpiredEmailVerificationTokens.DeleteExpiredEmailVerificationTokens;
Expand Down Expand Up @@ -257,7 +257,7 @@ private void init() throws IOException, StorageQueryException {
// starts DeleteExpiredAccessTokenSigningKeys cronjob if the access token signing keys can change
Cronjobs.addCronjob(this, DeleteExpiredAccessTokenSigningKeys.init(this, uniqueUserPoolIdsTenants));

Cronjobs.addCronjob(this, CleanupOAuthRevokeList.init(this, uniqueUserPoolIdsTenants));
Cronjobs.addCronjob(this, CleanupOAuthRevokeListAndChallenges.init(this, uniqueUserPoolIdsTenants));

// this is to ensure tenantInfos are in sync for the new cron job as well
MultitenancyHelper.getInstance(this).refreshCronjobs();
Expand Down
24 changes: 12 additions & 12 deletions src/main/java/io/supertokens/config/CoreConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public class CoreConfig {
"oauth_provider_public_service_url",
"oauth_provider_admin_service_url",
"oauth_provider_consent_login_base_url",
"oauth_provider_url_configured_in_hydra"
"oauth_provider_url_configured_in_oauth_provider"
};

@IgnoreForAnnotationCheck
Expand Down Expand Up @@ -297,15 +297,15 @@ public class CoreConfig {
@JsonProperty
@HideFromDashboard
@ConfigDescription(
"If specified, the core uses this URL replace the default consent and login URLs to {apiDomain}. Defaults to 'null'")
"If specified, the core uses this URL replace the default consent and login URLs to {apiDomain}.")
private String oauth_provider_consent_login_base_url = null;

@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 = null;
"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.")
private String oauth_provider_url_configured_in_oauth_provider = null;

@ConfigYamlOnly
@JsonProperty
Expand Down Expand Up @@ -373,11 +373,11 @@ public String getOauthProviderConsentLoginBaseUrl() throws InvalidConfigExceptio
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");
public String getOAuthProviderUrlConfiguredInOAuthProvider() throws InvalidConfigException {
if(oauth_provider_url_configured_in_oauth_provider == null) {
throw new InvalidConfigException("oauth_provider_url_configured_in_oauth_provider is not set");
}
return oauth_provider_url_configured_in_hydra;
return oauth_provider_url_configured_in_oauth_provider;
}

public String getIpAllowRegex() {
Expand Down Expand Up @@ -891,13 +891,13 @@ void normalizeAndValidate(Main main, boolean includeConfigFilePath) throws Inval
}


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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.supertokens.cronjobs.cleanupOAuthRevokeList;
package io.supertokens.cronjobs.cleanupOAuthRevokeListAndChallenges;

import java.util.List;

Expand All @@ -12,26 +12,27 @@
import io.supertokens.pluginInterface.oauth.OAuthStorage;
import io.supertokens.storageLayer.StorageLayer;

public class CleanupOAuthRevokeList extends CronTask {
public class CleanupOAuthRevokeListAndChallenges extends CronTask {

public static final String RESOURCE_KEY = "io.supertokens.cronjobs.cleanupOAuthRevokeList" +
".CleanupOAuthRevokeList";
public static final String RESOURCE_KEY = "io.supertokens.cronjobs.cleanupOAuthRevokeListAndChallenges" +
".CleanupOAuthRevokeListAndChallenges";

private CleanupOAuthRevokeList(Main main, List<List<TenantIdentifier>> tenantsInfo) {
private CleanupOAuthRevokeListAndChallenges(Main main, List<List<TenantIdentifier>> tenantsInfo) {
super("CleanupOAuthRevokeList", main, tenantsInfo, true);
}

public static CleanupOAuthRevokeList init(Main main, List<List<TenantIdentifier>> tenantsInfo) {
return (CleanupOAuthRevokeList) main.getResourceDistributor()
public static CleanupOAuthRevokeListAndChallenges init(Main main, List<List<TenantIdentifier>> tenantsInfo) {
return (CleanupOAuthRevokeListAndChallenges) main.getResourceDistributor()
.setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY,
new CleanupOAuthRevokeList(main, tenantsInfo));
new CleanupOAuthRevokeListAndChallenges(main, tenantsInfo));
}

@Override
protected void doTaskPerApp(AppIdentifier app) throws Exception {
Storage storage = StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main);
OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage);
oauthStorage.cleanUpExpiredAndRevokedTokens(app);
oauthStorage.deleteLogoutChallengesBefore(app, System.currentTimeMillis() - 1000 * 60 * 60 * 48);
}

@Override
Expand Down
38 changes: 38 additions & 0 deletions 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.OAuthLogoutChallenge;
import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage;
import io.supertokens.pluginInterface.passwordless.PasswordlessCode;
import io.supertokens.pluginInterface.passwordless.PasswordlessDevice;
Expand Down Expand Up @@ -3077,6 +3078,43 @@ public void addM2MToken(AppIdentifier appIdentifier, String clientId, long iat,
}
}

@Override
public void addLogoutChallenge(AppIdentifier appIdentifier, String challenge, String clientId,
String postLogoutRedirectionUri, String sessionHandle, String state, long timeCreated) throws StorageQueryException {
try {
OAuthQueries.addLogoutChallenge(this, appIdentifier, challenge, clientId, postLogoutRedirectionUri, sessionHandle, state, timeCreated);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
public OAuthLogoutChallenge getLogoutChallenge(AppIdentifier appIdentifier, String challenge) throws StorageQueryException {
try {
return OAuthQueries.getLogoutChallenge(this, appIdentifier, challenge);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
public void deleteLogoutChallenge(AppIdentifier appIdentifier, String challenge) throws StorageQueryException {
try {
OAuthQueries.deleteLogoutChallenge(this, appIdentifier, challenge);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
public void deleteLogoutChallengesBefore(AppIdentifier appIdentifier, long time) throws StorageQueryException {
try {
OAuthQueries.deleteLogoutChallengesBefore(this, appIdentifier, time);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
public void cleanUpExpiredAndRevokedTokens(AppIdentifier appIdentifier) throws StorageQueryException {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,8 @@ public String getOAuthRevokeTable() {
public String getOAuthM2MTokensTable() {
return "oauth_m2m_tokens";
}

public String getOAuthLogoutChallengesTable() {
return "oauth_logout_challenges";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,15 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc
update(start, OAuthQueries.getQueryToCreateOAuthM2MTokenIatIndex(start), NO_OP_SETTER);
update(start, OAuthQueries.getQueryToCreateOAuthM2MTokenExpIndex(start), NO_OP_SETTER);
}
}

if (!doesTableExists(start, Config.getConfig(start).getOAuthLogoutChallengesTable())) {
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, OAuthQueries.getQueryToCreateOAuthLogoutChallengesTable(start), NO_OP_SETTER);

// index
update(start, OAuthQueries.getQueryToCreateOAuthLogoutChallengesTimeCreatedIndex(start), NO_OP_SETTER);
}
}

public static void setKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier,
String key, KeyValueInfo info)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.supertokens.inmemorydb.config.Config;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.oauth.OAuthLogoutChallenge;

import java.sql.ResultSet;
import java.sql.SQLException;
Expand Down Expand Up @@ -92,6 +93,32 @@ public static String getQueryToCreateOAuthM2MTokenExpIndex(Start start) {
+ oAuth2M2MTokensTable + "(exp DESC, app_id DESC);";
}

public static String getQueryToCreateOAuthLogoutChallengesTable(Start start) {
String oAuth2LogoutChallengesTable = Config.getConfig(start).getOAuthLogoutChallengesTable();
// @formatter:off
return "CREATE TABLE IF NOT EXISTS " + oAuth2LogoutChallengesTable + " ("
+ "app_id VARCHAR(64) DEFAULT 'public',"
+ "challenge VARCHAR(128) NOT NULL,"
+ "client_id VARCHAR(128) NOT NULL,"
+ "post_logout_redirect_uri VARCHAR(1024),"
+ "session_handle VARCHAR(128),"
+ "state VARCHAR(128),"
+ "time_created BIGINT NOT NULL,"
+ "PRIMARY KEY (app_id, challenge),"
+ "FOREIGN KEY(app_id, client_id)"
+ " REFERENCES " + Config.getConfig(start).getOAuthClientsTable() + "(app_id, client_id) ON DELETE CASCADE,"
+ "FOREIGN KEY(app_id)"
+ " REFERENCES " + Config.getConfig(start).getAppsTable() + "(app_id) ON DELETE CASCADE"
+ ");";
// @formatter:on
}

public static String getQueryToCreateOAuthLogoutChallengesTimeCreatedIndex(Start start) {
String oAuth2LogoutChallengesTable = Config.getConfig(start).getOAuthLogoutChallengesTable();
return "CREATE INDEX IF NOT EXISTS oauth_logout_challenges_time_created_index ON "
+ oAuth2LogoutChallengesTable + "(time_created ASC, app_id ASC);";
}

public static boolean isClientIdForAppId(Start start, String clientId, AppIdentifier appIdentifier)
throws SQLException, StorageQueryException {
String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthClientsTable() +
Expand Down Expand Up @@ -164,7 +191,7 @@ public static void revoke(Start start, AppIdentifier appIdentifier, String targe
public static boolean isRevoked(Start start, AppIdentifier appIdentifier, String[] targetTypes, String[] targetValues, long issuedAt)
throws SQLException, StorageQueryException {
String QUERY = "SELECT app_id FROM " + Config.getConfig(start).getOAuthRevokeTable() +
" WHERE app_id = ? AND timestamp > ? AND (";
" WHERE app_id = ? AND timestamp >= ? AND (";

for (int i = 0; i < targetTypes.length; i++) {
QUERY += "(target_type = ? AND target_value = ?)";
Expand Down Expand Up @@ -285,4 +312,60 @@ public static void cleanUpExpiredAndRevokedTokens(Start start, AppIdentifier app
});
}
}

public static void addLogoutChallenge(Start start, AppIdentifier appIdentifier, String challenge, String clientId,
String postLogoutRedirectionUri, String sessionHandle, String state, long timeCreated) throws SQLException, StorageQueryException {
String QUERY = "INSERT INTO " + Config.getConfig(start).getOAuthLogoutChallengesTable() +
" (app_id, challenge, client_id, post_logout_redirect_uri, session_handle, state, time_created) VALUES (?, ?, ?, ?, ?, ?, ?)";
update(start, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, challenge);
pst.setString(3, clientId);
pst.setString(4, postLogoutRedirectionUri);
pst.setString(5, sessionHandle);
pst.setString(6, state);
pst.setLong(7, timeCreated);
});
}

public static OAuthLogoutChallenge getLogoutChallenge(Start start, AppIdentifier appIdentifier, String challenge) throws SQLException, StorageQueryException {
String QUERY = "SELECT challenge, client_id, post_logout_redirect_uri, session_handle, state, time_created FROM " +
Config.getConfig(start).getOAuthLogoutChallengesTable() +
" WHERE app_id = ? AND challenge = ?";

return execute(start, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, challenge);
}, result -> {
if (result.next()) {
return new OAuthLogoutChallenge(
result.getString("challenge"),
result.getString("client_id"),
result.getString("post_logout_redirect_uri"),
result.getString("session_handle"),
result.getString("state"),
result.getLong("time_created")
);
}
return null;
});
}

public static void deleteLogoutChallenge(Start start, AppIdentifier appIdentifier, String challenge) throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthLogoutChallengesTable() +
" WHERE app_id = ? AND challenge = ?";
update(start, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, challenge);
});
}

public static void deleteLogoutChallengesBefore(Start start, AppIdentifier appIdentifier, long time) throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthLogoutChallengesTable() +
" WHERE app_id = ? AND time_created < ?";
update(start, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setLong(2, time);
});
}
}
Loading

0 comments on commit 6f225c5

Please sign in to comment.