diff --git a/CHANGELOG.md b/CHANGELOG.md index bad5c336..e47580ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [9.4.2] + +- Fixes concurrency issue with oauth refresh token + ## [9.4.1] - Fixes env var reading with specific types diff --git a/build.gradle b/build.gradle index 5251cbe7..59e906a0 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "9.4.1" +version = "9.4.2" repositories { mavenCentral() diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index 6a4412be..65aa57fd 100644 --- a/pluginInterfaceSupported.json +++ b/pluginInterfaceSupported.json @@ -1,6 +1,6 @@ { "_comment": "contains a list of plugin interfaces branch names that this core supports", "versions": [ - "8.4" + "8.5" ] } diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index 3981bf69..6bfc32ba 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -17,39 +17,11 @@ package io.supertokens.storage.postgresql; -import java.lang.reflect.Field; -import java.sql.BatchUpdateException; -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.SQLTransactionRollbackException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.annotation.Nonnull; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; -import org.postgresql.util.PSQLException; -import org.postgresql.util.ServerErrorMessage; -import org.slf4j.LoggerFactory; - +import ch.qos.logback.classic.Logger; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.zaxxer.hikari.pool.HikariPool; - -import ch.qos.logback.classic.Logger; -import io.supertokens.pluginInterface.ActiveUsersSQLStorage; -import io.supertokens.pluginInterface.ActiveUsersStorage; -import io.supertokens.pluginInterface.ConfigFieldInfo; -import io.supertokens.pluginInterface.KeyValueInfo; -import io.supertokens.pluginInterface.LOG_LEVEL; -import io.supertokens.pluginInterface.RECIPE_ID; -import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.*; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; @@ -96,16 +68,13 @@ import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.pluginInterface.oauth.exception.DuplicateOAuthLogoutChallengeException; import io.supertokens.pluginInterface.oauth.exception.OAuthClientNotFoundException; +import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage; import io.supertokens.pluginInterface.opentelemetry.OtelProvider; import io.supertokens.pluginInterface.opentelemetry.WithinOtelSpan; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; import io.supertokens.pluginInterface.passwordless.PasswordlessImportUser; -import io.supertokens.pluginInterface.passwordless.exception.DuplicateCodeIdException; -import io.supertokens.pluginInterface.passwordless.exception.DuplicateDeviceIdHashException; -import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException; -import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException; -import io.supertokens.pluginInterface.passwordless.exception.UnknownDeviceIdHash; +import io.supertokens.pluginInterface.passwordless.exception.*; import io.supertokens.pluginInterface.passwordless.sqlStorage.PasswordlessSQLStorage; import io.supertokens.pluginInterface.saml.SAMLClaimsInfo; import io.supertokens.pluginInterface.saml.SAMLClient; @@ -140,43 +109,37 @@ import io.supertokens.pluginInterface.webauthn.AccountRecoveryTokenInfo; import io.supertokens.pluginInterface.webauthn.WebAuthNOptions; import io.supertokens.pluginInterface.webauthn.WebAuthNStoredCredential; -import io.supertokens.pluginInterface.webauthn.exceptions.DuplicateOptionsIdException; -import io.supertokens.pluginInterface.webauthn.exceptions.DuplicateRecoverAccountTokenException; -import io.supertokens.pluginInterface.webauthn.exceptions.DuplicateUserEmailException; -import io.supertokens.pluginInterface.webauthn.exceptions.WebauthNCredentialNotExistsException; -import io.supertokens.pluginInterface.webauthn.exceptions.WebauthNOptionsNotExistsException; +import io.supertokens.pluginInterface.webauthn.exceptions.*; import io.supertokens.pluginInterface.webauthn.slqStorage.WebAuthNSQLStorage; -import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; import io.supertokens.storage.postgresql.annotations.EnvName; import io.supertokens.storage.postgresql.config.Config; import io.supertokens.storage.postgresql.config.PostgreSQLConfig; import io.supertokens.storage.postgresql.output.Logging; -import io.supertokens.storage.postgresql.queries.ActiveUsersQueries; -import io.supertokens.storage.postgresql.queries.BulkImportQueries; -import io.supertokens.storage.postgresql.queries.DashboardQueries; -import io.supertokens.storage.postgresql.queries.EmailPasswordQueries; -import io.supertokens.storage.postgresql.queries.EmailVerificationQueries; -import io.supertokens.storage.postgresql.queries.GeneralQueries; -import io.supertokens.storage.postgresql.queries.JWTSigningQueries; -import io.supertokens.storage.postgresql.queries.MultitenancyQueries; -import io.supertokens.storage.postgresql.queries.OAuthQueries; -import io.supertokens.storage.postgresql.queries.PasswordlessQueries; -import io.supertokens.storage.postgresql.queries.SAMLQueries; -import io.supertokens.storage.postgresql.queries.SessionQueries; -import io.supertokens.storage.postgresql.queries.TOTPQueries; -import io.supertokens.storage.postgresql.queries.ThirdPartyQueries; -import io.supertokens.storage.postgresql.queries.UserIdMappingQueries; -import io.supertokens.storage.postgresql.queries.UserMetadataQueries; -import io.supertokens.storage.postgresql.queries.UserRolesQueries; -import io.supertokens.storage.postgresql.queries.WebAuthNQueries; +import io.supertokens.storage.postgresql.queries.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; +import org.postgresql.util.PSQLException; +import org.postgresql.util.ServerErrorMessage; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.lang.reflect.Field; +import java.sql.BatchUpdateException; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLTransactionRollbackException; +import java.util.*; + +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; @WithinOtelSpan public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, DashboardSQLStorage, TOTPSQLStorage, - ActiveUsersStorage, ActiveUsersSQLStorage, AuthRecipeSQLStorage, OAuthStorage, BulkImportSQLStorage, - WebAuthNSQLStorage, SAMLStorage { + ActiveUsersStorage, ActiveUsersSQLStorage, AuthRecipeSQLStorage, OAuthStorage, OAuthSQLStorage, + BulkImportSQLStorage, WebAuthNSQLStorage, SAMLStorage { // these configs are protected from being modified / viewed by the dev using the SuperTokens // SaaS. If the core is not running in SuperTokens SaaS, this array has no effect. @@ -4053,6 +4016,31 @@ public String getRefreshTokenMapping(AppIdentifier appIdentifier, String externa } } + @Override + public String getRefreshTokenMappingForUpdate_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String externalRefreshToken) + throws StorageQueryException { + try { + return OAuthQueries.getRefreshTokenMappingForUpdate(this, (Connection) con.getConnection(), + appIdentifier, externalRefreshToken); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void updateOAuthSessionInternal_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String gid, String newInternalRefreshToken, + String sessionHandle, String jti, long exp) + throws StorageQueryException { + try { + OAuthQueries.updateOAuthSessionInternal(this, (Connection) con.getConnection(), + appIdentifier, gid, newInternalRefreshToken, sessionHandle, jti, exp); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public void deleteExpiredOAuthSessions(long exp) throws StorageQueryException { try { diff --git a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java index e289f766..07728f73 100644 --- a/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java +++ b/src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java @@ -674,7 +674,7 @@ private void validateAndNormalise(boolean skipValidation) throws InvalidConfigEx { // postgresql_host if (postgresql_host == null) { - postgresql_host = "localhost"; + postgresql_host = System.getProperty("ST_POSTGRESQL_PLUGIN_SERVER_HOST", "localhost"); } } diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/OAuthQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/OAuthQueries.java index c6f59566..bd46449d 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/OAuthQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/OAuthQueries.java @@ -25,6 +25,7 @@ import io.supertokens.storage.postgresql.utils.Utils; import org.jetbrains.annotations.NotNull; +import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; @@ -428,6 +429,55 @@ public static String getRefreshTokenMapping(Start start, AppIdentifier appIdenti }); } + /** + * SELECT FOR UPDATE variant — must be called inside an open transaction. + * Locks the oauth_sessions row for the given externalRefreshToken so that no + * other DB client can read or write it until the transaction is committed or + * rolled back. + */ + public static String getRefreshTokenMappingForUpdate(Start start, Connection con, + AppIdentifier appIdentifier, + String externalRefreshToken) + throws SQLException, StorageQueryException { + String QUERY = "SELECT internal_refresh_token FROM " + Config.getConfig(start).getOAuthSessionsTable() + + " WHERE app_id = ? AND external_refresh_token = ? FOR UPDATE"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, externalRefreshToken); + }, result -> { + if (result.next()) { + return result.getString("internal_refresh_token"); + } + return null; + }); + } + + /** + * Updates the internal token and metadata for a non-rotating refresh. + * Must be called inside the same transaction that previously called + * {@link #getRefreshTokenMappingForUpdate}. + */ + public static void updateOAuthSessionInternal(Start start, Connection con, + AppIdentifier appIdentifier, + String gid, + String newInternalRefreshToken, + String sessionHandle, + String jti, + long exp) + throws SQLException, StorageQueryException { + String QUERY = "UPDATE " + Config.getConfig(start).getOAuthSessionsTable() + + " SET internal_refresh_token = ?, session_handle = ?, jti = CONCAT(jti, ?), exp = ?" + + " WHERE gid = ? AND app_id = ?"; + update(con, QUERY, pst -> { + pst.setString(1, newInternalRefreshToken); + pst.setString(2, sessionHandle); + pst.setString(3, jti + ","); + pst.setLong(4, exp); + pst.setString(5, gid); + pst.setString(6, appIdentifier.getAppId()); + }); + } + public static void deleteExpiredOAuthSessions(Start start, long exp) throws SQLException, StorageQueryException { // delete expired M2M tokens String QUERY = "DELETE FROM " + Config.getConfig(start).getOAuthSessionsTable() +