Skip to content

Commit 5eb8516

Browse files
authored
feat: SAML (#287)
* fix: saml impl * fix: remove sp entity id from client * fix: unique idp entity id * fix: changelog * fix: SAML client count * fix: index for expires_at * fix: repeatable read * fix: mt query serializable * fix: gradle * fix: update deadlock tests * fix: configurable claims and relay state validity * fix: restoring transaction isolation level for bulk import * fix: revert transaction isolation changes * fix: autocommit * fix: revert deadlock test * fix: auto commit * fix: restore isolation and auto commit
1 parent 5b624cc commit 5eb8516

File tree

9 files changed

+802
-75
lines changed

9 files changed

+802
-75
lines changed

CHANGELOG.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,67 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
99

10+
## [9.3.0]
11+
12+
- Adds SAML support
13+
14+
### Migration
15+
16+
```sql
17+
CREATE TABLE IF NOT EXISTS saml_clients (
18+
app_id VARCHAR(64) NOT NULL DEFAULT 'public',
19+
tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',
20+
client_id VARCHAR(256) NOT NULL,
21+
client_secret TEXT,
22+
sso_login_url TEXT NOT NULL,
23+
redirect_uris TEXT NOT NULL,
24+
default_redirect_uri TEXT NOT NULL,
25+
idp_entity_id VARCHAR(256) NOT NULL,
26+
idp_signing_certificate TEXT NOT NULL,
27+
allow_idp_initiated_login BOOLEAN NOT NULL DEFAULT FALSE,
28+
enable_request_signing BOOLEAN NOT NULL DEFAULT FALSE,
29+
created_at BIGINT NOT NULL,
30+
updated_at BIGINT NOT NULL,
31+
CONSTRAINT saml_clients_pkey PRIMARY KEY(app_id, tenant_id, client_id),
32+
CONSTRAINT saml_clients_idp_entity_id_key UNIQUE (app_id, tenant_id, idp_entity_id),
33+
CONSTRAINT saml_clients_app_id_fkey FOREIGN KEY(app_id) REFERENCES apps (app_id) ON DELETE CASCADE,
34+
CONSTRAINT saml_clients_tenant_id_fkey FOREIGN KEY(app_id, tenant_id) REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE
35+
);
36+
37+
CREATE INDEX IF NOT EXISTS saml_clients_app_id_tenant_id_index ON saml_clients (app_id, tenant_id);
38+
39+
CREATE TABLE IF NOT EXISTS saml_relay_state (
40+
app_id VARCHAR(64) NOT NULL DEFAULT 'public',
41+
tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',
42+
relay_state VARCHAR(256) NOT NULL,
43+
client_id VARCHAR(256) NOT NULL,
44+
state TEXT NOT NULL,
45+
redirect_uri TEXT NOT NULL,
46+
created_at BIGINT NOT NULL,
47+
CONSTRAINT saml_relay_state_pkey PRIMARY KEY(app_id, tenant_id, relay_state),
48+
CONSTRAINT saml_relay_state_app_id_fkey FOREIGN KEY(app_id) REFERENCES apps (app_id) ON DELETE CASCADE,
49+
CONSTRAINT saml_relay_state_tenant_id_fkey FOREIGN KEY(app_id, tenant_id) REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE
50+
);
51+
52+
CREATE INDEX IF NOT EXISTS saml_relay_state_app_id_tenant_id_index ON saml_relay_state (app_id, tenant_id);
53+
CREATE INDEX IF NOT EXISTS saml_relay_state_expires_at_index ON saml_relay_state (expires_at);
54+
55+
CREATE TABLE IF NOT EXISTS saml_claims (
56+
app_id VARCHAR(64) NOT NULL DEFAULT 'public',
57+
tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',
58+
client_id VARCHAR(256) NOT NULL,
59+
code VARCHAR(256) NOT NULL,
60+
claims TEXT NOT NULL,
61+
created_at BIGINT NOT NULL,
62+
CONSTRAINT saml_claims_pkey PRIMARY KEY(app_id, tenant_id, code),
63+
CONSTRAINT saml_claims_app_id_fkey FOREIGN KEY(app_id) REFERENCES apps (app_id) ON DELETE CASCADE,
64+
CONSTRAINT saml_claims_tenant_id_fkey FOREIGN KEY(app_id, tenant_id) REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE
65+
);
66+
67+
CREATE INDEX IF NOT EXISTS saml_claims_app_id_tenant_id_index ON saml_claims (app_id, tenant_id);
68+
CREATE INDEX IF NOT EXISTS saml_claims_expires_at_index ON saml_claims (expires_at);
69+
```
70+
1071
## [9.2.0]
1172

1273
- Adds docker support for opentelemetry javaagent

build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ plugins {
22
id 'java-library'
33
}
44

5-
version = "9.2.0"
5+
version = "9.3.0"
66

77
repositories {
88
mavenCentral()
9+
10+
maven { url 'https://build.shibboleth.net/nexus/content/repositories/releases/' }
911
}
1012

1113
dependencies {

pluginInterfaceSupported.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"_comment": "contains a list of plugin interfaces branch names that this core supports",
33
"versions": [
4-
"8.2"
4+
"8.3"
55
]
66
}

src/main/java/io/supertokens/storage/postgresql/BulkImportProxyStorage.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@
1616

1717
package io.supertokens.storage.postgresql;
1818

19+
import java.sql.Connection;
20+
import java.sql.SQLException;
21+
import java.util.List;
22+
1923
import io.supertokens.pluginInterface.exceptions.DbInitException;
2024
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
2125
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
2226
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
2327
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
2428
import io.supertokens.pluginInterface.sqlStorage.TransactionConnection;
2529

26-
import java.sql.Connection;
27-
import java.sql.SQLException;
28-
import java.util.List;
29-
3030

3131
/**
3232
* BulkImportProxyStorage is a class extending Start, serving as a Storage instance in the bulk import user cronjob.

src/main/java/io/supertokens/storage/postgresql/Start.java

Lines changed: 177 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,39 @@
1717

1818
package io.supertokens.storage.postgresql;
1919

20-
import ch.qos.logback.classic.Logger;
20+
import java.lang.reflect.Field;
21+
import java.sql.BatchUpdateException;
22+
import java.sql.Connection;
23+
import java.sql.SQLException;
24+
import java.sql.SQLTransactionRollbackException;
25+
import java.util.ArrayList;
26+
import java.util.HashMap;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.Set;
30+
31+
import javax.annotation.Nonnull;
32+
33+
import org.jetbrains.annotations.NotNull;
34+
import org.jetbrains.annotations.Nullable;
35+
import org.jetbrains.annotations.TestOnly;
36+
import org.postgresql.util.PSQLException;
37+
import org.postgresql.util.ServerErrorMessage;
38+
import org.slf4j.LoggerFactory;
39+
2140
import com.google.gson.JsonObject;
2241
import com.google.gson.JsonPrimitive;
2342
import com.zaxxer.hikari.pool.HikariPool;
24-
import io.supertokens.pluginInterface.*;
43+
44+
import ch.qos.logback.classic.Logger;
45+
import io.supertokens.pluginInterface.ActiveUsersSQLStorage;
46+
import io.supertokens.pluginInterface.ActiveUsersStorage;
47+
import io.supertokens.pluginInterface.ConfigFieldInfo;
48+
import io.supertokens.pluginInterface.KeyValueInfo;
49+
import io.supertokens.pluginInterface.LOG_LEVEL;
50+
import io.supertokens.pluginInterface.RECIPE_ID;
51+
import io.supertokens.pluginInterface.STORAGE_TYPE;
52+
import io.supertokens.pluginInterface.Storage;
2553
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
2654
import io.supertokens.pluginInterface.authRecipe.LoginMethod;
2755
import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage;
@@ -73,8 +101,16 @@
73101
import io.supertokens.pluginInterface.passwordless.PasswordlessCode;
74102
import io.supertokens.pluginInterface.passwordless.PasswordlessDevice;
75103
import io.supertokens.pluginInterface.passwordless.PasswordlessImportUser;
76-
import io.supertokens.pluginInterface.passwordless.exception.*;
104+
import io.supertokens.pluginInterface.passwordless.exception.DuplicateCodeIdException;
105+
import io.supertokens.pluginInterface.passwordless.exception.DuplicateDeviceIdHashException;
106+
import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException;
107+
import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException;
108+
import io.supertokens.pluginInterface.passwordless.exception.UnknownDeviceIdHash;
77109
import io.supertokens.pluginInterface.passwordless.sqlStorage.PasswordlessSQLStorage;
110+
import io.supertokens.pluginInterface.saml.SAMLClaimsInfo;
111+
import io.supertokens.pluginInterface.saml.SAMLClient;
112+
import io.supertokens.pluginInterface.saml.SAMLRelayStateInfo;
113+
import io.supertokens.pluginInterface.saml.SAMLStorage;
78114
import io.supertokens.pluginInterface.session.SessionInfo;
79115
import io.supertokens.pluginInterface.session.SessionStorage;
80116
import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage;
@@ -104,37 +140,43 @@
104140
import io.supertokens.pluginInterface.webauthn.AccountRecoveryTokenInfo;
105141
import io.supertokens.pluginInterface.webauthn.WebAuthNOptions;
106142
import io.supertokens.pluginInterface.webauthn.WebAuthNStoredCredential;
107-
import io.supertokens.pluginInterface.webauthn.exceptions.*;
143+
import io.supertokens.pluginInterface.webauthn.exceptions.DuplicateOptionsIdException;
144+
import io.supertokens.pluginInterface.webauthn.exceptions.DuplicateRecoverAccountTokenException;
145+
import io.supertokens.pluginInterface.webauthn.exceptions.DuplicateUserEmailException;
146+
import io.supertokens.pluginInterface.webauthn.exceptions.WebauthNCredentialNotExistsException;
147+
import io.supertokens.pluginInterface.webauthn.exceptions.WebauthNOptionsNotExistsException;
108148
import io.supertokens.pluginInterface.webauthn.slqStorage.WebAuthNSQLStorage;
149+
import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute;
109150
import io.supertokens.storage.postgresql.annotations.EnvName;
110151
import io.supertokens.storage.postgresql.config.Config;
111152
import io.supertokens.storage.postgresql.config.PostgreSQLConfig;
112153
import io.supertokens.storage.postgresql.output.Logging;
113-
import io.supertokens.storage.postgresql.queries.*;
114-
import org.jetbrains.annotations.NotNull;
115-
import org.jetbrains.annotations.Nullable;
116-
import org.jetbrains.annotations.TestOnly;
117-
import org.postgresql.util.PSQLException;
118-
import org.postgresql.util.ServerErrorMessage;
119-
import org.slf4j.LoggerFactory;
120-
121-
import javax.annotation.Nonnull;
122-
import java.lang.reflect.Field;
123-
import java.sql.BatchUpdateException;
124-
import java.sql.Connection;
125-
import java.sql.SQLException;
126-
import java.sql.SQLTransactionRollbackException;
127-
import java.util.*;
128-
129-
import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute;
154+
import io.supertokens.storage.postgresql.queries.ActiveUsersQueries;
155+
import io.supertokens.storage.postgresql.queries.BulkImportQueries;
156+
import io.supertokens.storage.postgresql.queries.DashboardQueries;
157+
import io.supertokens.storage.postgresql.queries.EmailPasswordQueries;
158+
import io.supertokens.storage.postgresql.queries.EmailVerificationQueries;
159+
import io.supertokens.storage.postgresql.queries.GeneralQueries;
160+
import io.supertokens.storage.postgresql.queries.JWTSigningQueries;
161+
import io.supertokens.storage.postgresql.queries.MultitenancyQueries;
162+
import io.supertokens.storage.postgresql.queries.OAuthQueries;
163+
import io.supertokens.storage.postgresql.queries.PasswordlessQueries;
164+
import io.supertokens.storage.postgresql.queries.SAMLQueries;
165+
import io.supertokens.storage.postgresql.queries.SessionQueries;
166+
import io.supertokens.storage.postgresql.queries.TOTPQueries;
167+
import io.supertokens.storage.postgresql.queries.ThirdPartyQueries;
168+
import io.supertokens.storage.postgresql.queries.UserIdMappingQueries;
169+
import io.supertokens.storage.postgresql.queries.UserMetadataQueries;
170+
import io.supertokens.storage.postgresql.queries.UserRolesQueries;
171+
import io.supertokens.storage.postgresql.queries.WebAuthNQueries;
130172

131173
@WithinOtelSpan
132174
public class Start
133175
implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage,
134176
JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage,
135177
UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, DashboardSQLStorage, TOTPSQLStorage,
136178
ActiveUsersStorage, ActiveUsersSQLStorage, AuthRecipeSQLStorage, OAuthStorage, BulkImportSQLStorage,
137-
WebAuthNSQLStorage {
179+
WebAuthNSQLStorage, SAMLStorage {
138180

139181
// these configs are protected from being modified / viewed by the dev using the SuperTokens
140182
// SaaS. If the core is not running in SuperTokens SaaS, this array has no effect.
@@ -1032,6 +1074,8 @@ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifi
10321074
//ignore
10331075
} else if (className.equals(OAuthStorage.class.getName())) {
10341076
/* Since OAuth recipe tables do not store userId we do not add any data to them */
1077+
} else if (className.equals(SAMLStorage.class.getName())) {
1078+
//ignore
10351079
} else if (className.equals(ActiveUsersStorage.class.getName())) {
10361080
try {
10371081
ActiveUsersQueries.updateUserLastActive(this, tenantIdentifier.toAppIdentifier(), userId);
@@ -4392,4 +4436,115 @@ public void deleteExpiredGeneratedOptions() throws StorageQueryException {
43924436
throw new StorageQueryException(e);
43934437
}
43944438
}
4439+
4440+
@Override
4441+
public SAMLClient createOrUpdateSAMLClient(TenantIdentifier tenantIdentifier, SAMLClient samlClient)
4442+
throws StorageQueryException, io.supertokens.pluginInterface.saml.exception.DuplicateEntityIdException {
4443+
try {
4444+
return SAMLQueries.createOrUpdateSAMLClient(this, tenantIdentifier, samlClient.clientId, samlClient.clientSecret,
4445+
samlClient.ssoLoginURL, samlClient.redirectURIs.toString(), samlClient.defaultRedirectURI,
4446+
samlClient.idpEntityId, samlClient.idpSigningCertificate,
4447+
samlClient.allowIDPInitiatedLogin, samlClient.enableRequestSigning);
4448+
} catch (SQLException e) {
4449+
if (e instanceof PSQLException) {
4450+
PostgreSQLConfig config = Config.getConfig(this);
4451+
ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage();
4452+
4453+
if (isUniqueConstraintError(serverMessage, config.getSAMLClientsTable(), "idp_entity_id")) {
4454+
throw new io.supertokens.pluginInterface.saml.exception.DuplicateEntityIdException();
4455+
}
4456+
}
4457+
throw new StorageQueryException(e);
4458+
}
4459+
}
4460+
4461+
@Override
4462+
public boolean removeSAMLClient(TenantIdentifier tenantIdentifier, String clientId) throws StorageQueryException {
4463+
try {
4464+
return SAMLQueries.removeSAMLClient(this, tenantIdentifier, clientId);
4465+
} catch (SQLException e) {
4466+
throw new StorageQueryException(e);
4467+
}
4468+
}
4469+
4470+
@Override
4471+
public SAMLClient getSAMLClient(TenantIdentifier tenantIdentifier, String clientId) throws StorageQueryException {
4472+
try {
4473+
return SAMLQueries.getSAMLClient(this, tenantIdentifier, clientId);
4474+
} catch (SQLException e) {
4475+
throw new StorageQueryException(e);
4476+
}
4477+
}
4478+
4479+
@Override
4480+
public SAMLClient getSAMLClientByIDPEntityId(TenantIdentifier tenantIdentifier, String idpEntityId) throws StorageQueryException {
4481+
try {
4482+
return SAMLQueries.getSAMLClientByIDPEntityId(this, tenantIdentifier, idpEntityId);
4483+
} catch (SQLException e) {
4484+
throw new StorageQueryException(e);
4485+
}
4486+
}
4487+
4488+
@Override
4489+
public List<SAMLClient> getSAMLClients(TenantIdentifier tenantIdentifier) throws StorageQueryException {
4490+
try {
4491+
return SAMLQueries.getSAMLClients(this, tenantIdentifier);
4492+
} catch (SQLException e) {
4493+
throw new StorageQueryException(e);
4494+
}
4495+
}
4496+
4497+
@Override
4498+
public void saveRelayStateInfo(TenantIdentifier tenantIdentifier, SAMLRelayStateInfo relayStateInfo, long relayStateValidity) throws StorageQueryException {
4499+
try {
4500+
SAMLQueries.saveRelayStateInfo(this, tenantIdentifier, relayStateInfo.relayState, relayStateInfo.clientId, relayStateInfo.state, relayStateInfo.redirectURI, relayStateValidity);
4501+
} catch (SQLException e) {
4502+
throw new StorageQueryException(e);
4503+
}
4504+
}
4505+
4506+
@Override
4507+
public SAMLRelayStateInfo getRelayStateInfo(TenantIdentifier tenantIdentifier, String relayState) throws StorageQueryException {
4508+
try {
4509+
return SAMLQueries.getRelayStateInfo(this, tenantIdentifier, relayState);
4510+
} catch (SQLException e) {
4511+
throw new StorageQueryException(e);
4512+
}
4513+
}
4514+
4515+
@Override
4516+
public void saveSAMLClaims(TenantIdentifier tenantIdentifier, String clientId, String code, JsonObject claims, long claimsValidity) throws StorageQueryException {
4517+
try {
4518+
SAMLQueries.saveSAMLClaims(this, tenantIdentifier, clientId, code, claims.toString(), claimsValidity);
4519+
} catch (SQLException e) {
4520+
throw new StorageQueryException(e);
4521+
}
4522+
}
4523+
4524+
@Override
4525+
public SAMLClaimsInfo getSAMLClaimsAndRemoveCode(TenantIdentifier tenantIdentifier, String code) throws StorageQueryException {
4526+
try {
4527+
return SAMLQueries.getSAMLClaimsAndRemoveCode(this, tenantIdentifier, code);
4528+
} catch (SQLException e) {
4529+
throw new StorageQueryException(e);
4530+
}
4531+
}
4532+
4533+
@Override
4534+
public void removeExpiredSAMLCodesAndRelayStates() throws StorageQueryException {
4535+
try {
4536+
SAMLQueries.removeExpiredSAMLCodesAndRelayStates(this);
4537+
} catch (SQLException e) {
4538+
throw new StorageQueryException(e);
4539+
}
4540+
}
4541+
4542+
@Override
4543+
public int countSAMLClients(TenantIdentifier tenantIdentifier) throws StorageQueryException {
4544+
try {
4545+
return SAMLQueries.countSAMLClients(this, tenantIdentifier);
4546+
} catch (SQLException e) {
4547+
throw new StorageQueryException(e);
4548+
}
4549+
}
43954550
}

src/main/java/io/supertokens/storage/postgresql/config/PostgreSQLConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,12 @@ public String getOAuthLogoutChallengesTable() {
496496

497497
public String getWebAuthNAccountRecoveryTokenTable() { return addSchemaAndPrefixToTableName("webauthn_account_recovery_tokens"); }
498498

499+
public String getSAMLClientsTable() { return addSchemaAndPrefixToTableName("saml_clients"); }
500+
501+
public String getSAMLRelayStateTable() { return addSchemaAndPrefixToTableName("saml_relay_state"); }
502+
503+
public String getSAMLClaimsTable() { return addSchemaAndPrefixToTableName("saml_claims"); }
504+
499505
public String getBulkImportUsersTable() {
500506
return addSchemaAndPrefixToTableName("bulk_import_users");
501507
}

0 commit comments

Comments
 (0)