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

fix: Multitenant session #619

Merged
merged 18 commits into from
Apr 5, 2023
248 changes: 118 additions & 130 deletions src/main/java/io/supertokens/session/Session.java

Large diffs are not rendered by default.

17 changes: 14 additions & 3 deletions src/main/java/io/supertokens/session/accessToken/AccessToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ private static AccessTokenInfo getInfoFromAccessToken(AppIdentifier appIdentifie
throw new TryRefreshTokenException("Access token expired");
}

// There is no need to check if the appIdentifier (from request) is the same app in which the
// accessToken was created, because, each app has a different accessTokenSigningKey and
// when a cross app request is made, token decoding will fail and result in TRY_REFRESH_TOKEN
// Hence, we don't bother storing any info related to app in the accessTokenPayload.
return new AccessTokenInfo(tokenInfo.sessionHandle, tokenInfo.userId, tokenInfo.refreshTokenHash1,
tokenInfo.expiryTime, tokenInfo.parentRefreshTokenHash1, tokenInfo.userData, tokenInfo.antiCsrfToken,
tokenInfo.timeCreated, tokenInfo.lmrt,
Expand Down Expand Up @@ -155,14 +159,21 @@ public static AccessTokenInfo getInfoFromAccessToken(AppIdentifier appIdentifier
}

@TestOnly
public static AccessTokenInfo getInfoFromAccessTokenWithoutVerifying(@Nonnull String token) {
return getInfoFromAccessTokenWithoutVerifying(new AppIdentifier(null, null), token);
public static AccessTokenInfo getInfoFromAccessTokenWithoutVerifying(Main main, @Nonnull String token) {
try {
return getInfoFromAccessTokenWithoutVerifying(
new AppIdentifier(null, null), token);
} catch (TenantOrAppNotFoundException | NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}

rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
public static AccessTokenInfo getInfoFromAccessTokenWithoutVerifying(AppIdentifier appIdentifier,
@Nonnull String token) {
@Nonnull String token)
throws TenantOrAppNotFoundException, NoSuchAlgorithmException {
AccessTokenPayload tokenInfo = new Gson().fromJson(JWT.getPayloadWithoutVerifying(token).payload,
AccessTokenPayload.class);

return new AccessTokenInfo(tokenInfo.sessionHandle, tokenInfo.userId, tokenInfo.refreshTokenHash1,
tokenInfo.expiryTime, tokenInfo.parentRefreshTokenHash1, tokenInfo.userData, tokenInfo.antiCsrfToken,
tokenInfo.timeCreated, tokenInfo.lmrt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,23 +150,35 @@ synchronized void removeKeyFromMemoryIfItHasNotChanged(List<KeyInfo> oldKeyInfo)

public synchronized void transferLegacyKeyToNewTable()
throws StorageQueryException, StorageTransactionLogicException, TenantOrAppNotFoundException {
Storage storage = StorageLayer.getSessionStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main);
Storage storage = StorageLayer.getStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main);

if (storage.getType() == STORAGE_TYPE.SQL) {
SessionSQLStorage sqlStorage = (SessionSQLStorage) storage;

// start transaction
sqlStorage.startTransaction(con -> {
KeyValueInfo legacyKey = sqlStorage.getLegacyAccessTokenSigningKey_Transaction(
appIdentifier, con);
try {
// start transaction
sqlStorage.startTransaction(con -> {
KeyValueInfo legacyKey = sqlStorage.getLegacyAccessTokenSigningKey_Transaction(
appIdentifier, con);

if (legacyKey != null) {
sqlStorage.addAccessTokenSigningKey_Transaction(appIdentifier, con, legacyKey);
sqlStorage.removeLegacyAccessTokenSigningKey_Transaction(appIdentifier, con);
sqlStorage.commitTransaction(con);
if (legacyKey != null) {
try {
sqlStorage.addAccessTokenSigningKey_Transaction(appIdentifier, con, legacyKey);
} catch (TenantOrAppNotFoundException e) {
throw new StorageTransactionLogicException(e);
}
sqlStorage.removeLegacyAccessTokenSigningKey_Transaction(appIdentifier, con);
sqlStorage.commitTransaction(con);
}
return legacyKey;
});
} catch (StorageTransactionLogicException e) {
if (e.actualException instanceof TenantOrAppNotFoundException) {
throw (TenantOrAppNotFoundException) e.actualException;
}
return legacyKey;
});
throw e;
}

} else {
SessionNoSQLStorage_1 noSQLStorage = (SessionNoSQLStorage_1) storage;
KeyValueInfoWithLastUpdated legacyKey = noSQLStorage.getLegacyAccessTokenSigningKey_Transaction();
Expand All @@ -186,7 +198,7 @@ public synchronized void transferLegacyKeyToNewTable()

public synchronized void cleanExpiredAccessTokenSigningKeys() throws StorageQueryException,
TenantOrAppNotFoundException {
SessionStorage storage = StorageLayer.getSessionStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main);
SessionStorage storage = (SessionStorage) StorageLayer.getStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main);
CoreConfig config = Config.getConfig(this.appIdentifier.getAsPublicTenantIdentifier(), main);

if (config.getAccessTokenSigningKeyDynamic()) {
Expand Down Expand Up @@ -232,7 +244,7 @@ public synchronized long getKeyExpiryTime()

private List<KeyInfo> maybeGenerateNewKeyAndUpdateInDb()
throws StorageQueryException, StorageTransactionLogicException, TenantOrAppNotFoundException {
Storage storage = StorageLayer.getSessionStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main);
Storage storage = StorageLayer.getStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main);
CoreConfig config = Config.getConfig(this.appIdentifier.getAsPublicTenantIdentifier(), main);

// Access token signing keys older than this are deleted (ms)
Expand All @@ -250,42 +262,54 @@ private List<KeyInfo> maybeGenerateNewKeyAndUpdateInDb()
if (storage.getType() == STORAGE_TYPE.SQL) {
SessionSQLStorage sqlStorage = (SessionSQLStorage) storage;

// start transaction
validKeys = sqlStorage.startTransaction(con -> {
List<KeyInfo> validKeysFromSQL = new ArrayList<KeyInfo>();

// We have to generate a new key if we couldn't find one we can use for signing
boolean generateNewKey = true;

KeyValueInfo[] keysFromStorage = sqlStorage.getAccessTokenSigningKeys_Transaction(appIdentifier,
con);

for (KeyValueInfo key : keysFromStorage) {
if (keysCreatedAfterCanVerify <= key.createdAtTime) {
if (keysCreatedAfterCanSign <= key.createdAtTime) {
generateNewKey = false;
try {
// start transaction
validKeys = sqlStorage.startTransaction(con -> {
List<KeyInfo> validKeysFromSQL = new ArrayList<KeyInfo>();

// We have to generate a new key if we couldn't find one we can use for signing
boolean generateNewKey = true;

KeyValueInfo[] keysFromStorage = sqlStorage.getAccessTokenSigningKeys_Transaction(appIdentifier,
con);

for (KeyValueInfo key : keysFromStorage) {
if (keysCreatedAfterCanVerify <= key.createdAtTime) {
if (keysCreatedAfterCanSign <= key.createdAtTime) {
generateNewKey = false;
}
validKeysFromSQL.add(new KeyInfo(key.value, key.createdAtTime, signingKeyLifetime));
}
validKeysFromSQL.add(new KeyInfo(key.value, key.createdAtTime, signingKeyLifetime));
}
}

if (generateNewKey) {
String signingKey;
try {
Utils.PubPriKey rsaKeys = Utils.generateNewPubPriKey();
signingKey = rsaKeys.toString();
} catch (NoSuchAlgorithmException e) {
throw new StorageTransactionLogicException(e);
if (generateNewKey) {
String signingKey;
try {
Utils.PubPriKey rsaKeys = Utils.generateNewPubPriKey();
signingKey = rsaKeys.toString();
} catch (NoSuchAlgorithmException e) {
throw new StorageTransactionLogicException(e);
}
KeyInfo newKey = new KeyInfo(signingKey, System.currentTimeMillis(), signingKeyLifetime);
try {
sqlStorage.addAccessTokenSigningKey_Transaction(appIdentifier, con,
new KeyValueInfo(newKey.value, newKey.createdAtTime));
} catch (TenantOrAppNotFoundException e) {
throw new StorageTransactionLogicException(e);
}
validKeysFromSQL.add(newKey);
}
KeyInfo newKey = new KeyInfo(signingKey, System.currentTimeMillis(), signingKeyLifetime);
sqlStorage.addAccessTokenSigningKey_Transaction(appIdentifier, con,
new KeyValueInfo(newKey.value, newKey.createdAtTime));
validKeysFromSQL.add(newKey);

sqlStorage.commitTransaction(con);
return validKeysFromSQL;
});
} catch (StorageTransactionLogicException e) {
if (e.actualException instanceof TenantOrAppNotFoundException) {
throw (TenantOrAppNotFoundException) e.actualException;
}
throw e;
}

sqlStorage.commitTransaction(con);
return validKeysFromSQL;
});
} else if (storage.getType() == STORAGE_TYPE.NOSQL_1) {
SessionNoSQLStorage_1 noSQLStorage = (SessionNoSQLStorage_1) storage;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public static RefreshTokenInfo getInfoFromRefreshToken(AppIdentifier appIdentifi
|| !nonce.equals(tokenPayload.nonce)) {
throw new UnauthorisedException("Invalid refresh token");
}

return new RefreshTokenInfo(tokenPayload.sessionHandle, tokenPayload.userId,
tokenPayload.parentRefreshTokenHash1, null, tokenPayload.antiCsrfToken, tokenType,
new TenantIdentifier(appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,34 +126,46 @@ public String getKey() throws StorageQueryException, StorageTransactionLogicExce

private String maybeGenerateNewKeyAndUpdateInDb()
throws StorageQueryException, StorageTransactionLogicException, TenantOrAppNotFoundException {
SessionStorage storage = StorageLayer.getSessionStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main);
SessionStorage storage = (SessionStorage) StorageLayer.getStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main);

if (storage.getType() == STORAGE_TYPE.SQL) {

SessionSQLStorage sqlStorage = (SessionSQLStorage) storage;

// start transaction
return sqlStorage.startTransaction(con -> {
String key = null;
KeyValueInfo keyFromStorage = sqlStorage.getRefreshTokenSigningKey_Transaction(appIdentifier, con);
if (keyFromStorage != null) {
key = keyFromStorage.value;
}
try {
// start transaction
return sqlStorage.startTransaction(con -> {
String key = null;
KeyValueInfo keyFromStorage = sqlStorage.getRefreshTokenSigningKey_Transaction(appIdentifier, con);
if (keyFromStorage != null) {
key = keyFromStorage.value;
}

if (key == null) {
try {
key = Utils.generateNewSigningKey();
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new StorageTransactionLogicException(e);
if (key == null) {
try {
key = Utils.generateNewSigningKey();
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new StorageTransactionLogicException(e);
}
try {
sqlStorage.setRefreshTokenSigningKey_Transaction(appIdentifier, con,
new KeyValueInfo(key, System.currentTimeMillis()));
} catch (TenantOrAppNotFoundException e) {
throw new StorageTransactionLogicException(e);
}
}
sqlStorage.setRefreshTokenSigningKey_Transaction(appIdentifier, con,
new KeyValueInfo(key, System.currentTimeMillis()));
}

sqlStorage.commitTransaction(con);
return key;
sqlStorage.commitTransaction(con);
return key;

});
} catch (StorageTransactionLogicException e) {
if (e.actualException instanceof TenantOrAppNotFoundException) {
throw (TenantOrAppNotFoundException) e.actualException;
}
throw e;
}

});
} else if (storage.getType() == STORAGE_TYPE.NOSQL_1) {
SessionNoSQLStorage_1 noSQLStorage = (SessionNoSQLStorage_1) storage;

Expand Down
16 changes: 0 additions & 16 deletions src/main/java/io/supertokens/storageLayer/StorageLayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -328,22 +328,6 @@ public static AuthRecipeStorage getAuthRecipeStorage(Main main) {
}
}

public static SessionStorage getSessionStorage(TenantIdentifier tenantIdentifier, Main main)
throws TenantOrAppNotFoundException {
// TODO remove this function
return (SessionStorage) getInstance(tenantIdentifier, main).storage;
}

@TestOnly
public static SessionStorage getSessionStorage(Main main) {
// TODO remove this function
try {
return getSessionStorage(new TenantIdentifier(null, null, null), main);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
}

public static JWTRecipeStorage getJWTRecipeStorage(TenantIdentifier tenantIdentifier, Main main)
throws TenantOrAppNotFoundException {
// TODO remove this function
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/supertokens/webserver/WebserverAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws
sendTextResponse(401, "Invalid API key", resp);
} else if (rootCause instanceof TenantOrAppNotFoundException) {
sendTextResponse(400,
"AppId or tenantId not found => " + ((TenantOrAppNotFoundException) e).getMessage(),
"AppId or tenantId not found => " + ((TenantOrAppNotFoundException) rootCause).getMessage(),
resp);
} else if (rootCause instanceof BadPermissionException) {
sendTextResponse(403, e.getMessage(), resp);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.session.accessToken.AccessTokenSigningKey;
import io.supertokens.session.accessToken.AccessTokenSigningKey.KeyInfo;
Expand All @@ -49,35 +50,34 @@ public String getPath() {

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
// API is tenant specific
try {
JsonObject result = new JsonObject();
result.addProperty("status", "OK");

TenantIdentifier tenantIdentifier = this.getTenantIdentifierWithStorageFromRequest(req);

result.addProperty("jwtSigningPublicKey",
new Utils.PubPriKey(
AccessTokenSigningKey.getInstance(this.getTenantIdentifierWithStorageFromRequest(req).toAppIdentifier(),
AccessTokenSigningKey.getInstance(tenantIdentifier.toAppIdentifier(),
main).getLatestIssuedKey().value).publicKey);
result.addProperty("jwtSigningPublicKeyExpiryTime",
AccessTokenSigningKey.getInstance(this.getTenantIdentifierWithStorageFromRequest(req).toAppIdentifier(), main)
AccessTokenSigningKey.getInstance(tenantIdentifier.toAppIdentifier(), main)
.getKeyExpiryTime());

if (!super.getVersionFromRequest(req).equals("2.7") && !super.getVersionFromRequest(req).equals("2.8")) {
List<KeyInfo> keys = AccessTokenSigningKey.getInstance(this.getTenantIdentifierWithStorageFromRequest(req).toAppIdentifier(),
main)
List<KeyInfo> keys = AccessTokenSigningKey.getInstance(tenantIdentifier.toAppIdentifier(), main)
.getAllKeys();
JsonArray jwtSigningPublicKeyListJSON = Utils.keyListToJson(keys);
result.add("jwtSigningPublicKeyList", jwtSigningPublicKeyListJSON);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure all the props here are not per app?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

properties seems to be fine

result.addProperty("accessTokenBlacklistingEnabled",
Config.getConfig(this.getTenantIdentifierWithStorageFromRequest(req), main)
.getAccessTokenBlacklisting());
Config.getConfig(tenantIdentifier, main).getAccessTokenBlacklisting());
result.addProperty("accessTokenValidity",
Config.getConfig(this.getTenantIdentifierWithStorageFromRequest(req), main)
.getAccessTokenValidity());
Config.getConfig(tenantIdentifier, main).getAccessTokenValidity());
result.addProperty("refreshTokenValidity",
Config.getConfig(this.getTenantIdentifierWithStorageFromRequest(req), main)
.getRefreshTokenValidity());
Config.getConfig(tenantIdentifier, main).getRefreshTokenValidity());
super.sendJsonResponse(200, result, resp);
} catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException e) {
throw new ServletException(e);
Expand Down
Loading