Compare commits

...

142 Commits
10.0 ... master

Author SHA1 Message Date
Supertokens Bot c9e8287b30 adding dev-v11.3.0 tag to this commit to ensure building 2025-11-19 14:05:30 +00:00
Sattvik Chakravarthy 5f2e57ba31
feat: SAML (#1192)
* fix: outline

* fix: login redirect impl

* fix: handle SAML callback

* fix: saml cert management

* fix: authn request signing

* fix: working login for azure

* fix: save idp entity id

* fix: use client id from relay state info

* fix: create or update saml client

* fix: generate clientId

* fix: list SAML Clients

* fix: remove saml client

* fix: saml callback and token

* fix: idp flow

* fix: remove unnecessary logging

* fix: apis to work like boxy

* fix: add support for legacy SAML ACS URL and enhance SAML client management

* fix: enforce public tenant in legacy APIs

* fix: client secret checking in legacy API

* fix: cronjob to cleanup saml codes

* fix: tests

* fix: version update

* test: create or update saml client

* test: list and delete saml client

* test: create saml login redirect

* test: bad inputs for handle saml callback

* fix: expiration handling

* test: SAML audience check

* fix: enable request signing

* fix: remove metadata url and add enable request signing

* fix: remaining tests

* fix: idp flow tests

* fix: tests

* fix: remove sp entity id from client

* fix: saml feature check

* fix: unique idp entity id

* fix: sp metadata and featureflag test

* fix: tests

* fix: global logging level

* fix: changelog

* fix: SAML client count

* fix: saml stats

* fix: SAML certificate refresh

* fix: SAML metadata API

* fix: tests

* fix: not loading keys on tenant creation

* fix: deadlock

* fix: removing deadlock causing code

* fix: removing locks

* Revert "fix: deadlock"

This reverts commit 2d5a07c7e0.

* fix: index for expires_at

* fix: rename saml cleanup cron task

* experiment: Deadlock logger (#1198)

* experiment: Deadlock logger

* fix: race issue with oauth refresh (#1199)

* fix: race issue with oauth refresh

* fix: review comment

* fix: remove print

* fix: deadlock in resource distributor (#1197)

* adding dev-v11.2.1 tag to this commit to ensure building

* fix: add deadlock logger

* fix: changelog and build version

* fix: only start deadlocklogger if it's enabled

---------

Co-authored-by: Sattvik Chakravarthy <sattvik@supertokens.com>
Co-authored-by: Supertokens Bot <>

* fix: tests

* fix: tests

* fix: inmemory tests

* fix: gradle

* fix: deadlock in delete table

* fix: in memory test for concurrency

* fix: configurable claims and relay state validity and cleanup

* fix: generating secure random for serial number

* fix: bulk import chunking

* fix: revert lock related changes

* fix: auto commit

* fix: revert bulk import

* fix: auto commit

---------

Co-authored-by: Tamas Soltesz <tamas@supertokens.com>
2025-11-19 19:30:41 +05:30
Supertokens Bot bce323172f adding dev-v11.2.1 tag to this commit to ensure building 2025-11-12 11:42:19 +00:00
Sattvik Chakravarthy 1312069b0d
fix: deadlock in resource distributor (#1197) 2025-11-12 17:01:52 +05:30
Sattvik Chakravarthy 1b1b2b2bdf
fix: race issue with oauth refresh (#1199)
* fix: race issue with oauth refresh

* fix: review comment

* fix: remove print
2025-11-12 16:22:42 +05:30
Supertokens Bot e2ce5828f0 adding dev-v11.2.0 tag to this commit to ensure building 2025-10-28 07:42:28 +00:00
Tamas Soltesz 8ee1665c72
feat: otel javaagent (#1189)
* feat: add java cli options for javaagent and jmx

* fix: gitignore changes to allow resources jars

* feat: add otel javaagent jars

* fix: loading agent from the agent dir

* fix: changelog and build version

* fix: proper agent attach command on unix-like systems

* feat: aspect for wrapping methods in otel span

* fix: add exception records to spans

* fix: use javaagent's otel if present

* fix: fixing agent loading at startup

* fix: add logging for the command that's being run

* fix: pinpoint runner image version

* fix: pinpoint runner image version

* fix: tell the core branch name for the workflow

* fix: build docker image with the right branches

* fix: publish-dev-docker formatting

* fix: using an other variable for deciding the current branch name

* fix: using an other variable for deciding the current branch name

* fix: update cli dependencies to make it the same version as core

* fix: update implementationDependencies.json with aspectjrt

* fix: update build version

---------

Co-authored-by: Sattvik Chakravarthy <sattvik@supertokens.com>
2025-10-28 13:08:57 +05:30
Supertokens Bot d524bdb9d9 adding dev-v11.1.1 tag to this commit to ensure building 2025-10-22 10:48:20 +00:00
Sattvik Chakravarthy be57314b35
Update dev-tag.yml 2025-10-22 16:15:43 +05:30
Supertokens Bot 34eeda50fe adding dev-v11.1.1 tag to this commit to ensure building 2025-10-22 10:41:31 +00:00
Sattvik Chakravarthy 058f6f69df
Change run-for parameter in dev-tag workflow 2025-10-22 16:08:51 +05:30
Supertokens Bot e671bcb492 adding dev-v11.1.1 tag to this commit to ensure building 2025-10-22 10:14:14 +00:00
Tamas Soltesz c4e8afbcd1
fix: update tomcat-embed to go beyond known cves (#1193) 2025-10-22 14:41:16 +05:30
Supertokens Bot 384d688b85 adding dev-v11.1.0 tag to this commit to ensure building 2025-09-01 07:11:10 +00:00
Supertokens Bot 97dfa59faf adding dev-v11.1.0 tag to this commit to ensure building 2025-08-29 10:57:12 +00:00
Sattvik Chakravarthy 9d3253f71e
fix: test slowness due to invalid otel url (#1184)
* fix: test slowness due to invalid otel url

* fix: domain fix and changelog
2025-08-29 16:24:06 +05:30
Supertokens Bot 9b84d8cdef adding dev-v11.1.0 tag to this commit to ensure building 2025-08-28 12:17:46 +00:00
Tamas Soltesz 879292770a
feat: add hikari logs to otel (#1181)
* feat: add hikari logs to otel

* fix: add env name annotations

* fix: update core config and config mapper to load from env

* chore: update build version and changelog

* fix: config test

* fix: config json from env

* fix: changelog

* fix: storage class loader

---------

Co-authored-by: Sattvik Chakravarthy <sattvik@gmail.com>
2025-08-28 17:40:51 +05:30
Supertokens Bot 58956c3675 adding dev-v11.0.5 tag to this commit to ensure building 2025-07-29 12:49:03 +00:00
Tamas Soltesz d924ed3e44
fix: log earlier the error (#1173) 2025-07-29 18:15:05 +05:30
Supertokens Bot ceef7921f2 adding dev-v11.0.5 tag to this commit to ensure building 2025-07-28 14:03:19 +00:00
Tamas Soltesz 7444015f3d
fix: publish keys (#1172)
* fix: regenerate implementationDependencies.json

* fix: using different environment for publish related actions

* fix: syntax error

---------

Co-authored-by: Sattvik Chakravarthy <sattvik@supertokens.com>
2025-07-28 19:29:49 +05:30
Supertokens Bot f82174d398 adding dev-v11.0.5 tag to this commit to ensure building 2025-07-28 13:16:00 +00:00
Supertokens Bot 15933a3216 adding dev-v11.0.5 tag to this commit to ensure building 2025-07-28 10:33:24 +00:00
Tamas Soltesz c1e3ba7516
fix: regenerate implementationDependencies.json (#1171) 2025-07-28 15:20:43 +05:30
Tamas Soltesz 018877e803
feat: logs to telemetry (#1160)
* feat: initial otel provider

* feat: logs to telemetry

* chore: changelog and version

* Update src/main/java/io/supertokens/telemetry/TelemetryProvider.java

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Revert "Update src/main/java/io/supertokens/telemetry/TelemetryProvider.java"

This reverts commit 41a1e1f56a.

* fix: add new config to config and devConfig.yamls, revert improper change

* fix: fixing failing tests because of conflicting multiple initialization of otel

* fix: bigger padding around intervals for testing

* fix: bigger padding around intervals for testing

* feat: add possibility to start and end spans with child-parent relations

* chore: changing version and changelog

* fix: initialize otel sooner

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-07-28 15:14:45 +05:30
Sattvik Chakravarthy 7a75827d61
fix: container security check cron (#1158) 2025-07-28 14:43:46 +05:30
Tamas Soltesz 5fbe206e50
fix: CVE fixes (#1159)
* fix: upgrade tomcat to 11.0.8

* chore: build version and changelog
2025-07-28 14:41:51 +05:30
Mihály Lengyel a07e41591a
Merge pull request #1161 from supertokens/fix/missing_dependencies_json
fix: missing dependencies json
2025-07-24 00:18:13 +02:00
tamassoltesz f09f011e21 fix: remove duplicates 2025-07-17 14:46:59 +02:00
tamassoltesz f67feb4884 chore: changelog and build version 2025-07-17 13:05:45 +02:00
tamassoltesz 7c0d335967 fix: add implementationDependencies.json 2025-07-17 13:03:54 +02:00
tamassoltesz 497c1f677a fix: reverting implementationDependencies.json generation while building 2025-07-17 13:00:42 +02:00
tamassoltesz 5970840f9e fix: generating implementationDependencies.json when running build 2025-07-17 10:20:18 +02:00
tamassoltesz b9e05884a4 experimenting 2025-07-15 09:27:33 +02:00
Supertokens Bot 378498b979 adding dev-v11.0.4 tag to this commit to ensure building 2025-06-10 16:58:25 +00:00
Mihály Lengyel 2f31ec4a1c
fix: bulk migration user to roles association in case of non primary user (#1157)
## Summary of change

(A few sentences about this PR)

## Related issues

- Link to issue1 here
- Link to issue1 here

## Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your
changes work. Bonus points for screenshots and videos!)

## Documentation changes

(If relevant, please create a PR in our [docs repo](https://github.com/supertokens/docs), or create a checklist here
highlighting the necessary changes)

## Checklist for important updates

- [ ] Changelog has been updated
    - [ ] If there are any db schema changes, mention those changes clearly
- [ ] `coreDriverInterfaceSupported.json` file has been updated (if needed)
- [ ] `pluginInterfaceSupported.json` file has been updated (if needed)
- [ ] Changes to the version if needed
    - In `build.gradle`
- [ ] If added a new paid feature, edit the `getPaidFeatureStats` function in FeatureFlag.java file
- [ ] Had installed and ran the pre-commit hook
- [ ] If there are new dependencies that have been added in `build.gradle`, please make sure to add them
  in `implementationDependencies.json`.
- [ ] Update function `getValidFields` in `io/supertokens/config/CoreConfig.java` if new aliases were added for any core
  config (similar to the `access_token_signing_key_update_interval` config alias).
- [ ] Issue this PR against the latest non released version branch.
    - To know which one it is, run find the latest released tag (`git tag`) in the format `vX.Y.Z`, and then find the
      latest branch (`git branch --all`) whose `X.Y` is greater than the latest released tag.
    - If no such branch exists, then create one from the latest released branch.
- [ ] If added a foreign key constraint on `app_id_to_user_id` table, make sure to delete from this table when deleting
  the user as well if `deleteUserIdMappingToo` is false.
- [ ] If added a new recipe, then make sure to update the bulk import API to include the new recipe.

## Remaining TODOs for this PR

- [ ] Item1
- [ ] Item2
2025-06-10 17:03:46 +02:00
Mihály Lengyel 1cedc88a94
Merge pull request #1102 from mavwolverine/master
docs(README.md): updated github link for Viraj Kanwade
2025-06-10 16:29:01 +02:00
Tamas Soltesz 6b3b882b3f
fix: response check in test
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-06-10 16:20:18 +02:00
tamassoltesz 923e70eca4 chore: build version and changelog 2025-06-10 16:14:54 +02:00
tamassoltesz 64b421a130 fix: bulk migration user to role association id handling in case of non primary users 2025-06-10 16:12:35 +02:00
Supertokens Bot b1b98c35f0 adding dev-v11.0.3 tag to this commit to ensure building 2025-06-03 11:56:29 +00:00
Tamas Soltesz 00dbb22236
chore: remove implementationdependencies json (#1156)
* chore: remove implementationdependencies json

* chore: remove implementationdependencies json

---------

Co-authored-by: Sattvik Chakravarthy <sattvik@supertokens.com>
2025-06-03 17:20:41 +05:30
Tamas Soltesz 2496a96d42
fix: bulk migration stuck in processing (#1155)
* fix: additional error handling for email verification when bulk migrating

* fix: proper checking of batchupdateexception

* chore: changelog and build version update

* fix: change test data generation

* fix: clear up debug messages

* fix: fix tests
2025-06-03 17:20:09 +05:30
Supertokens Bot e76fbbc31c adding dev-v11.0.2 tag to this commit to ensure building 2025-05-22 05:46:23 +00:00
Tamas Soltesz a90148d0eb
fix: getUserByAccountInfo fix to consider tenantId instead of appId (#1153) 2025-05-22 11:13:59 +05:30
Supertokens Bot 68711923e6 adding dev-v11.0.1 tag to this commit to ensure building 2025-05-15 05:07:03 +00:00
Sattvik Chakravarthy faaabe8db2
fix: logback vulnerability (#1151) 2025-05-15 10:34:01 +05:30
Supertokens Bot d2c2bd4a45 adding dev-v11.0.1 tag to this commit to ensure building 2025-05-15 03:04:07 +00:00
Supertokens Bot c68631d6af adding dev-v11.0.1 tag to this commit to ensure building 2025-05-15 02:34:53 +00:00
Sattvik Chakravarthy b71264b18e fix: register new plugin script 2025-05-15 08:02:11 +05:30
Sattvik Chakravarthy 495a3f25cb fix: supertokens-root branch in add-dev-tag 2025-05-15 07:53:08 +05:30
Tamas Soltesz a877255eaf
fix: upgrade tomcat to 11.0.6 (#1150) 2025-05-15 07:47:00 +05:30
Supertokens Bot f09908dc14 adding dev-v11.0.0 tag to this commit to ensure building 2025-05-10 14:17:55 +00:00
Sattvik Chakravarthy 29782b00a2 fix: release workflow 2025-05-10 19:43:59 +05:30
Supertokens Bot a135b46346 adding dev-v11.0.0 tag to this commit to ensure building 2025-05-10 11:08:10 +00:00
Sattvik Chakravarthy d46136027f fix: release workflow 2025-05-10 00:45:02 +05:30
Supertokens Bot 91fcafe339 adding dev-v11.0.0 tag to this commit to ensure building 2025-05-09 15:17:51 +00:00
Sattvik Chakravarthy 12e2f04c4c
fix: release workflows (#1147)
* fix: add platforms to docker

* fix: release workflows
2025-05-09 18:09:49 +05:30
Sattvik Chakravarthy ebeb635cfb fix: create new plugin version 2025-05-09 11:44:43 +05:30
Sattvik Chakravarthy e665994285 fix: workflow dependencies 2025-05-09 11:26:35 +05:30
Sattvik Chakravarthy 72961d814f fix: cover version api call 2025-05-09 11:23:51 +05:30
Supertokens Bot e0e39af30b adding dev-v11.0.0 tag to this commit to ensure building 2025-05-08 16:31:06 +00:00
Sattvik Chakravarthy f1cc209f8f fix: from json 2025-05-08 21:59:00 +05:30
Supertokens Bot be24450387 adding dev-v11.0.0 tag to this commit to ensure building 2025-05-08 14:52:18 +00:00
Sattvik Chakravarthy 650006295e fix: script path 2025-05-08 19:47:14 +05:30
Sattvik Chakravarthy 999b1021ad fix: script path 2025-05-08 19:45:34 +05:30
Sattvik Chakravarthy 6bd62b884b fix: workflow condition 2025-05-08 19:44:09 +05:30
Sattvik Chakravarthy a964e003aa fix: workflow condition 2025-05-08 19:24:03 +05:30
Sattvik Chakravarthy 6b02c7becd fix: workflow condition 2025-05-08 19:20:24 +05:30
Sattvik Chakravarthy 9430e2fcb4 fix: workflow condition 2025-05-08 19:14:59 +05:30
Supertokens Bot e13612adec adding dev-v11.0.0 tag to this commit to ensure building 2025-05-08 13:41:54 +00:00
Sattvik Chakravarthy c6122ad2fd fix: workflow condition 2025-05-08 19:08:24 +05:30
Supertokens Bot 3e2c1c021d adding dev-v11.0.0 tag to this commit to ensure building 2025-05-08 13:35:19 +00:00
Sattvik Chakravarthy 36a280bff3 fix: workflow rules 2025-05-08 19:03:07 +05:30
Supertokens Bot bd691696a3 adding dev-v11.0.0 tag to this commit to ensure building 2025-05-08 13:25:05 +00:00
Sattvik Chakravarthy 5d0c345246 fix: condition 2025-05-08 18:52:29 +05:30
Supertokens Bot 3d52c9d1b8 adding dev-v11.0.0 tag to this commit to ensure building 2025-05-08 13:08:34 +00:00
Supertokens Bot c12901cd84 adding dev-v11.0.0 tag to this commit to ensure building 2025-05-08 12:57:48 +00:00
Sattvik Chakravarthy d2131cf974
fix: workflows (#1146) 2025-05-08 18:18:44 +05:30
Supertokens Bot 6e5a36cc1e adding dev-v11.0.0 tag to this commit to ensure building 2025-05-08 12:39:08 +00:00
Tamas Soltesz c6acdbd814
feat: gha java upgrade merge (#1145)
* fix: remove unnecessary process start and stop in tests

* fix: usermetadata and userroles

* fix: delete app in kill

* fix: email password tests

* fix: email verification tests

* fix: dashboard tests

* fix: account linking tests

* fix: passwordless tests

* fix: passwordless tests

* fix: thirdparty tests

* fix: jwt tests

* fix: mfa tests

* fix: oauth tests

* fix: webauthn tests

* fix: useridmapping tests

* fix: totp tests

* fix: session tests

* fix: auth recipe tests

* fix: refactor testing process and fix all tests

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: unit test gha

* fix: mongo tests

* fix: mongo tests

* fix: gha

* fix: flaky

* fix: compile error

* fix: tests

* fix: for mongo

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: disable speed test

* fix: one million users test

* fix: test update

* fix: sqlite test

* fix: tests

* fix: working test parallelisation for in-memory

* fix: more parallelism

* fix: trying arm

* fix: ubuntu

* fix: ee tests

* fix: increase retry

* fix: ee tests

* fix: parallelised postgres tests

* fix: tests

* fix: tests

* fix: workflow

* fix: tests

* fix: tests

* fix: tests

* fix: mysql for test

* fix: tests

* fix: tests

* fix: add mongo service

* fix: add mongo service

* fix: services

* fix: changelog and version

* fix: docker build

* fix: docker build

* fix: docker build

* fix: docker build

* fix: docker build

* fix: release workflow

* feat: update gradle

* fix: release workflow

* fix: release workflow

* fix: release workflow

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: stress tests

* fix: release workflow

* fix: compile error

* fix: compile error

* fix: release

* fix: release

* feat: update java and tomcat and gradle

* fix: gha refactor

* fix: gha refactor

* fix: gha refactor

* fix: gha refactor

* fix: gha refactor

* fix: change jdk/jre versions

* fix: change java version in GHA test

* fix: upgrade java version in GHA

* fix: change root branch

* fix: oauth tests

* fix: workflows

* fix: add dev tag

* fix: dev docker image

* fix: build versions

* fix: disable circle ci and fix release workflows

---------

Co-authored-by: Sattvik <sattvik@Sattviks-MacBook-Pro.local>
Co-authored-by: Sattvik Chakravarthy <sattvik@gmail.com>
Co-authored-by: Sattvik Chakravarthy <sattvik@supertokens.com>
2025-05-08 17:02:00 +05:30
Sattvik Chakravarthy c18965126f adding dev-v10.1.4 tag to this commit to ensure building 2025-05-01 16:22:12 +05:30
Tamas Soltesz dd8aa42c0d
fix: bulk import external userid error translation (#1144)
* fix: fixes null pointer exception in case of missing externalUserId in bulkImport

* chore: add changelog
2025-04-30 11:09:25 +05:30
Tamas Soltesz affa77169c
fix: bulk migration user roles association fix when there is no externalId (#1139)
Co-authored-by: Sattvik Chakravarthy <sattvik@supertokens.com>
2025-04-28 13:10:07 +05:30
Tamas Soltesz 33f28e1bc6
fix: bulk migration to actually use the input value for the email verification (#1137) 2025-04-28 13:09:02 +05:30
Sattvik 96def240b3 adding dev-v10.1.3 tag to this commit to ensure building 2025-04-02 16:36:38 +05:30
Sattvik Chakravarthy f45952d99f
fix: version bump (#1135)
Co-authored-by: Sattvik <sattvik@Sattviks-MacBook-Pro.local>
2025-04-02 16:34:32 +05:30
Sattvik bd5a279b11 adding dev-v10.1.2 tag to this commit to ensure building 2025-04-02 16:24:52 +05:30
Mihaly Lengyel 1ef697f925 adding dev-v10.1.2 tag to this commit to ensure building 2025-03-27 23:16:46 +01:00
Mihály Lengyel 205fe3dd4e
Merge pull request #1133 from supertokens/fix/dependency_versions
fix: dependency versions
2025-03-27 23:15:55 +01:00
tamassoltesz 482b0dbfcb chore: update dependencies, fix implementationDependencies.json 2025-03-27 23:04:38 +01:00
Sattvik abf8a2b65a adding dev-v10.1.2 tag to this commit to ensure building 2025-03-26 18:36:12 +05:30
Tamas Soltesz 37de0a8e3a
fix: add user_roles index to the right place (#1132) 2025-03-26 18:35:02 +05:30
Sattvik f0c7e5f8fd adding dev-v10.1.2 tag to this commit to ensure building 2025-03-26 17:11:55 +05:30
Tamas Soltesz 06c176d243
fix: bulk migration logging, tests, related query optimizations (#1131) 2025-03-26 15:55:55 +05:30
Mihaly Lengyel 2607c84352 adding dev-v10.1.1 tag to this commit to ensure building 2025-03-21 15:20:37 +01:00
Mihály Lengyel 5abba306f9
Merge pull request #1129 from supertokens/fix/bulk_migration_webauthn_user_tests
fix: bulk migration webauthn user tests
2025-03-21 15:18:39 +01:00
tamassoltesz ab7375aa92 fix: add test cases for bulk migration account linking with conflicting webauthn users 2025-03-21 15:12:26 +01:00
Mihaly Lengyel 7e93048a35 adding dev-v10.1.1 tag to this commit to ensure building 2025-03-21 00:05:56 +01:00
Mihály Lengyel 10768f7ee9
Merge pull request #1124 from supertokens/fix/bulk_migration_debug_logs
fix: bulk migration debug logs
2025-03-20 23:52:56 +01:00
Mihály Lengyel 0171fcd388
Merge branch '10.1' into fix/bulk_migration_debug_logs 2025-03-20 23:52:45 +01:00
Mihály Lengyel aeef6e3406
Merge pull request #1125 from supertokens/feat/bulk_migration_return_userids
feat: bulk migration return userids
2025-03-20 23:50:10 +01:00
Mihály Lengyel 611b5bc2e1
Merge pull request #1127 from supertokens/fix/bulk_migration_account_linking
fix: bulk migration account linking
2025-03-20 23:24:04 +01:00
tamassoltesz 0c03d9fd70 fix: review fixes + more tests 2025-03-20 22:51:38 +01:00
tamassoltesz 25fccc6849 fix: add more tests for bulk migration account linking behaviour 2025-03-20 21:51:15 +01:00
tamassoltesz a949c86750 fix: add more tests for bulk migration account linking behaviour 2025-03-20 16:52:57 +01:00
tamassoltesz 1f7e58d2ba fix: add more tests for bulk migration account linking behaviour 2025-03-20 16:47:10 +01:00
tamassoltesz 1efe87fe53 fix: add more tests for bulk import 2025-03-20 15:39:06 +01:00
tamassoltesz 122b574941 fix: add note for account takeover check during bulk migration 2025-03-20 15:26:36 +01:00
tamassoltesz 9561424777 fix: resolving leftover TODO 2025-03-20 14:11:53 +01:00
tamassoltesz 89f38fd7ff feat: speed up account linking while bulk inserting 2025-03-20 14:08:45 +01:00
tamassoltesz 5bfa99251a fix: remove leftover print statements 2025-03-19 13:32:06 +01:00
tamassoltesz 3755ed1f3c chore: changelog 2025-03-18 11:56:16 +01:00
tamassoltesz edaaf19e72 feat: bulk migration user import return ids 2025-03-18 11:55:03 +01:00
tamassoltesz 85118ac3ee feat: bulkimport upload returns userIds 2025-03-14 12:30:57 +01:00
tamassoltesz f2be9beb56 fix: remove unnecessary spacing from changelog 2025-03-13 21:31:55 +01:00
tamassoltesz 16b17beeaa fix: add debug logs for bulk migration 2025-03-13 21:28:31 +01:00
Mihaly Lengyel 9e24d0ea00 adding dev-v10.1.0 tag to this commit to ensure building 2025-03-13 15:50:37 +01:00
Mihály Lengyel 3c879c4273
Merge pull request #1123 from supertokens/fix/config_description_space
fix: config description space
2025-03-13 15:50:03 +01:00
tamassoltesz ff95845f75 fix: add space to description 2025-03-13 15:43:53 +01:00
Mihaly Lengyel 27aae9f205 adding dev-v10.1.0 tag to this commit to ensure building 2025-03-13 15:10:46 +01:00
Mihály Lengyel 59f8673979
Merge pull request #1122 from supertokens/fix/bm_test_fix
fix: bulk migration test fix
2025-03-13 15:09:44 +01:00
tamassoltesz c9b120d35a fix: restore empty env after test run 2025-03-13 15:03:35 +01:00
Mihaly Lengyel c394e1bac8 adding dev-v10.1.0 tag to this commit to ensure building 2025-03-13 14:21:08 +01:00
Mihály Lengyel 54d97dd569
Merge pull request #1121 from supertokens/feat/bulk_migration_improvements
feat: bulk migration improvements
2025-03-13 14:18:04 +01:00
tamassoltesz 61a67f5479 fix: additional empty checks for BM 2025-03-13 13:02:16 +01:00
tamassoltesz ea63eafdf0 fix: fix collection error messages for bulk migration / import user 2025-03-13 12:26:30 +01:00
tamassoltesz 5693b4b0aa fix: add test for plaintext password user bulk migration 2025-03-13 12:19:19 +01:00
tamassoltesz e91afcad8e fix: add test for env variable controlled cron loading 2025-03-13 12:14:25 +01:00
tamassoltesz f69477a520 feat: bulk migration cron control from env var 2025-03-13 11:29:39 +01:00
tamassoltesz 2d31f3a254 fix: deleting bogus test 2025-03-13 10:57:37 +01:00
tamassoltesz 6de2cbb3c1 Merge branch 'fix/bulk_migration_plaintextpassword' into feat/bulk_migration_improvements 2025-03-13 10:56:43 +01:00
tamassoltesz d14426a203 feat: bulk_migration_batch_size core config 2025-03-13 10:55:22 +01:00
Sattvik Chakravarthy 2baba1a43e adding dev-v10.1.0 tag to this commit to ensure building 2025-03-12 15:01:30 +05:30
Sattvik Chakravarthy aaa3992e4e
test: more webauthn tests (#1120)
* test: webauthn account recovery tests

* fix: webauthn: allow non-recipe users in response for get user from token

* fix: tests

---------

Co-authored-by: tamassoltesz <tamas@supertokens.com>
2025-03-12 15:00:54 +05:30
Sattvik Chakravarthy cc75411bf7 adding dev-v10.1.0 tag to this commit to ensure building 2025-03-12 01:34:04 +05:30
Tamas Soltesz 9d871a6260
fix: upgrade webauthn4j dependency (#1119) 2025-03-12 01:33:25 +05:30
Sattvik Chakravarthy ab109d3370 adding dev-v10.1.0 tag to this commit to ensure building 2025-03-11 23:59:43 +05:30
Tamas Soltesz 177ee2f49a
feat: webauthn base (#1115)
* feat: new dependency: webauthn4j

* feat: add tables for webauthn

* fix: typo fixes

* feat: webauthn options

* feat: registercredentials wip

* feat: passkeys register credentials wip

* feat: recipe user sign up

* recipe user creation wip

* sign up recipe user

* feat: register credentials

* fix: temp

* feat: webauthn support wip

* feat: webauthn support wip

* merging

* feat: webauthn support wip

* feat: getuserinfolist draft

* feat: get user by account info - webauthn support

* fix: generate account recovery token api

* feat: get user by account info - webauthn support

* feat: signup with credentialsregister

* fix: fixes for tests

* fix: fixes for tests

* feat: get generated options api

* feat: webauthn sign in

* fix: account recovery

* fix: fixes for tests

* fix: fixing id name in response

* fix: fixing id encoding in response

* fix: base64 url encode the challenge insted of base64 encode

* fix: account recovery impl

* fix: base64 encoding changes

* fix: fixes for tests

* fix: fixing sql issues and encoding issues

* fix: fixes for tests

* fix: integration fix for signup

* fix: webauthn flow test stub

* fix: fixes for sdk tests

* feat: add webauthn recover account apis to webserver

* feat: crud apis addition

* feat: remove options api

* fix: additional field in the sign in options response

* fix: reworked error handling

* fix: fixing GET api not to expect json body

* fix: sign in + options check

* fix: more descriptive error messages for credentialsRegister

* fix: typo fixes

* fix: fixes for tests

* fix: not letting dependencies exception to leak out

* feat: clean up expired data cron

* fix: changing recovery token consume

* fix: fixing loginmethod collection

* fix: signin fixes

* fix: webauthn sign in fixes

* fix: add recipeUserId in signIn response

* feat: enable credentials listing api

* feat: extending user listing with webauthn

* fix: don't use the counter at signin check

* fix: small fixes

* fix: setting UV and RK to false

* feat: saving userVerification and userPresence values

* fix: change a bunch of error messages for sdk integration

* fix: change a bunch of error messages for sdk integration

* fix: include userVerification and userPresence in options response

* fix: error messages changes

* fix: refactor exceptions

* feat: get credential api

* fix: options generation no longer throws invalid options error as per reference impl

* fix: more error handling for sign in

* fix: rename methods for better readability

* fix: throw the right exception

* ci: experiment with a GHA to publish test/dev images

* ci: experiment with publishing dev docker images

* ci: experiment with publishing dev docker images

* ci: experiment with a GHA to publish test/dev images

* fix: options validation

* fix: options validation rpId doesn't have to be an url

* fix: additional validation

* fix: fixes for various sdk tests

* fix: Dockerfile setupTestEnv --local

* ci: remove arm64 build from dev-docker

* fix: add webauth4jn-test dependency

* fix: fixing email verification query for webauthn

* fix: authenticator mocking and example usage

* fix: sem ver and few test fixes

* fix: test fixes

* fix: cdi version increment in webauthn test

* fix: webauthn signIn should load all loginmethods of the user

* fix: fixing table locked issue with in memory db

* fix: remove unnecessary logging

* fix: add tests and fixes

* fix: add tests and fixes for email update

* fix: additional tests and fixes related to useridmapping

* fix: add null check

* fix: add test

* fix: additional indexes for performance optimization

* ci: fix dev-docker build

* fix: self-review fixes

* fix: update pluginInterfaceSupported to the right branch

* chore: changelog, version number

* fix: review fixes

* fix: review fixes

* fix: fixing email verified flag after email change

* fix: review fixes

* fix: handling potential error while saving options

* fix: review fixes

* chore: updating supported pluginInterface

* chore: updating supported pluginInterface

* test: API tests (#1118)

* fix: API tests template

* fix: options register APIs

* test: register credential

* test: fix

* fix: test get credential

* test: list credential

* test: remove credential

* test: remove credential

* test: sign in options

* test: sign-in

* test: sign-in

* test: update email

* fix: delete

* fix: tests for mongodb

* fix: tests

* fix: tests

* fix: review fixes

* fix: review fix: token generation changes

---------

Co-authored-by: Sattvik Chakravarthy <sattvik@gmail.com>
Co-authored-by: Mihaly Lengyel <mihaly@lengyel.tech>
Co-authored-by: Sattvik Chakravarthy <sattvik@supertokens.com>
2025-03-11 23:54:51 +05:30
tamassoltesz 36f60799ed fix: pass plainTextPassword for processing too 2025-03-10 15:27:18 +01:00
Viraj Kanwade 4db84200fa docs(README.md): updated github link for Viraj Kanwade 2025-01-13 21:55:08 -08:00
517 changed files with 26716 additions and 4800 deletions

View File

@ -1,8 +1,9 @@
FROM ubuntu:16.04
FROM ubuntu:22.04
RUN apt-get update && apt-get upgrade -y
RUN apt-get update -y
#&& apt-get upgrade -y
RUN apt-get install build-essential -y
RUN apt-get install build-essential -y --fix-missing
RUN echo "mysql-server mysql-server/root_password password root" | debconf-set-selections
@ -12,7 +13,7 @@ RUN apt install mysql-server -y
RUN usermod -d /var/lib/mysql/ mysql
RUN mkdir /var/run/mysqld
RUN [ -d /var/run/mysqld ] || mkdir -p /var/run/mysqld
ADD ./runMySQL.sh /runMySQL.sh
@ -22,36 +23,42 @@ RUN apt-get install -y git-core
RUN apt-get install -y wget
# Install OpenJDK 12
RUN wget https://download.java.net/java/GA/jdk12.0.2/e482c34c86bd4bf8b56c0b35558996b9/10/GPL/openjdk-12.0.2_linux-x64_bin.tar.gz
RUN mkdir /usr/java
RUN mv openjdk-12.0.2_linux-x64_bin.tar.gz /usr/java
RUN cd /usr/java && tar -xzvf openjdk-12.0.2_linux-x64_bin.tar.gz
RUN echo 'JAVA_HOME=/usr/java/jdk-12.0.2' >> /etc/profile
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
RUN apt-get install jq -y
RUN apt-get install curl -y
RUN apt-get install unzip -y
# Install OpenJDK 15.0.1
RUN wget https://download.java.net/java/GA/jdk15.0.1/51f4f36ad4ef43e39d0dfdbaf6549e32/9/GPL/openjdk-15.0.1_linux-x64_bin.tar.gz
# Install OpenJDK 21.0.7
RUN wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz
RUN mv openjdk-15.0.1_linux-x64_bin.tar.gz /usr/java
RUN cd /usr/java && tar -xzvf openjdk-15.0.1_linux-x64_bin.tar.gz
RUN echo 'JAVA_HOME=/usr/java/jdk-15.0.1' >> /etc/profile
RUN mv OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz /usr/java
RUN cd /usr/java && tar -xzvf OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz -C /usr/java/
RUN mv /usr/java/jdk-21.0.7+6 /usr/java/jdk-21.0.7
RUN echo 'JAVA_HOME=/usr/java/jdk-21.0.7' >> /etc/profile
RUN echo 'JRE_HOME=/usr/java/jdk-21.0.7' >> /etc/profile
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
RUN echo 'export JAVA_HOME' >> /etc/profile
RUN echo 'export JRE_HOME' >> /etc/profile
RUN echo 'export PATH' >> /etc/profile
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-12.0.2/bin/java" 1
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-12.0.2/bin/javac" 1
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-21.0.7/bin/java" 1
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-21.0.7/bin/javac" 1
#install postgres 13
# Import Repository Signing Key
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata
RUN apt install curl gpg gnupg2 software-properties-common apt-transport-https lsb-release ca-certificates sudo -y
# Add PostgreSQL repository
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
# Update again
RUN apt update
# Install PostgreSQL 13
RUN apt install -y postgresql-13
# Verify PostgreSQL 13 Installation on Ubuntu 22.04|20.04|18.04
RUN psql --version
# Manage PostgreSQL 13 service
#you can manage with `service postgresql start`

View File

@ -0,0 +1,57 @@
FROM ubuntu:16.04
RUN apt-get update && apt-get upgrade -y
RUN apt-get install build-essential -y
RUN echo "mysql-server mysql-server/root_password password root" | debconf-set-selections
RUN echo "mysql-server mysql-server/root_password_again password root" | debconf-set-selections
RUN apt install mysql-server -y
RUN usermod -d /var/lib/mysql/ mysql
RUN mkdir /var/run/mysqld
ADD ./runMySQL.sh /runMySQL.sh
RUN chmod +x /runMySQL.sh
RUN apt-get install -y git-core
RUN apt-get install -y wget
# Install OpenJDK 12
RUN wget https://download.java.net/java/GA/jdk12.0.2/e482c34c86bd4bf8b56c0b35558996b9/10/GPL/openjdk-12.0.2_linux-x64_bin.tar.gz
RUN mkdir /usr/java
RUN mv openjdk-12.0.2_linux-x64_bin.tar.gz /usr/java
RUN cd /usr/java && tar -xzvf openjdk-12.0.2_linux-x64_bin.tar.gz
RUN echo 'JAVA_HOME=/usr/java/jdk-12.0.2' >> /etc/profile
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
RUN apt-get install jq -y
RUN apt-get install curl -y
RUN apt-get install unzip -y
# Install OpenJDK 21.0.7
RUN wget https://download.java.net/java/GA/jdk21.0.7/51f4f36ad4ef43e39d0dfdbaf6549e32/9/GPL/openjdk-21.0.7_linux-x64_bin.tar.gz
RUN mv openjdk-21.0.7_linux-x64_bin.tar.gz /usr/java
RUN cd /usr/java && tar -xzvf openjdk-21.0.7_linux-x64_bin.tar.gz
RUN echo 'JAVA_HOME=/usr/java/jdk-21.0.7' >> /etc/profile
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
RUN echo 'export JAVA_HOME' >> /etc/profile
RUN echo 'export JRE_HOME' >> /etc/profile
RUN echo 'export PATH' >> /etc/profile
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-12.0.2/bin/java" 1
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-12.0.2/bin/javac" 1

View File

@ -22,39 +22,29 @@ RUN apt-get install -y git-core
RUN apt-get install -y wget
# Install OpenJDK 12
RUN wget https://download.java.net/java/GA/jdk12.0.2/e482c34c86bd4bf8b56c0b35558996b9/10/GPL/openjdk-12.0.2_linux-x64_bin.tar.gz
RUN mkdir /usr/java
RUN mv openjdk-12.0.2_linux-x64_bin.tar.gz /usr/java
RUN cd /usr/java && tar -xzvf openjdk-12.0.2_linux-x64_bin.tar.gz
RUN echo 'JAVA_HOME=/usr/java/jdk-12.0.2' >> /etc/profile
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
RUN apt-get install jq -y
RUN apt-get install curl -y
RUN apt-get install unzip -y
# Install OpenJDK 15.0.1
RUN wget https://download.java.net/java/GA/jdk15.0.1/51f4f36ad4ef43e39d0dfdbaf6549e32/9/GPL/openjdk-15.0.1_linux-x64_bin.tar.gz
# Install OpenJDK 21.0.7
RUN wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz
RUN mv openjdk-15.0.1_linux-x64_bin.tar.gz /usr/java
RUN mv OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz /usr/java
RUN mkdir -p /usr/java/jdk-21.0.7
RUN cd /usr/java && tar -xzvf OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz -C /usr/java/jdk-21.0.7
RUN cd /usr/java && tar -xzvf openjdk-15.0.1_linux-x64_bin.tar.gz
RUN echo 'JAVA_HOME=/usr/java/jdk-15.0.1' >> /etc/profile
RUN echo 'JAVA_HOME=/usr/java/jdk-21.0.7' >> /etc/profile
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
RUN echo 'export JAVA_HOME' >> /etc/profile
RUN echo 'export JRE_HOME' >> /etc/profile
RUN echo 'export PATH' >> /etc/profile
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-12.0.2/bin/java" 1
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-12.0.2/bin/javac" 1
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-21.0.7/bin/java" 1
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-21.0.7/bin/javac" 1
#install postgres 13
# Import Repository Signing Key

View File

@ -138,8 +138,8 @@ do
cd supertokens-root
rm gradle.properties
update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 2
update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 2
update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-21.0.7/bin/java" 2
update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-21.0.7/bin/javac" 2
coreX=$(cut -d'.' -f1 <<<"$coreVersion")
coreY=$(cut -d'.' -f2 <<<"$coreVersion")

View File

@ -4,20 +4,22 @@ RUN apt-get update && apt-get upgrade -y
RUN apt-get install build-essential -y
RUN apt-get install -y git-core wget unzip jq curl
# Install OpenJDK 15.0.1
RUN wget https://download.java.net/java/GA/jdk15.0.1/51f4f36ad4ef43e39d0dfdbaf6549e32/9/GPL/openjdk-15.0.1_linux-x64_bin.tar.gz
RUN mkdir -p /usr/java
RUN mv openjdk-15.0.1_linux-x64_bin.tar.gz /usr/java
RUN cd /usr/java && tar -xzvf openjdk-15.0.1_linux-x64_bin.tar.gz
# Install OpenJDK 21.0.7
RUN wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz
RUN echo 'JAVA_HOME=/usr/java/jdk-15.0.1' >> /etc/profile
RUN mv OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz /usr/java
RUN mkdir -p /usr/java/
RUN cd /usr/java && tar -xzvf OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz
RUN mv /usr/java/jdk-21.0.7+6 /usr/java/jdk-21.0.7
RUN echo 'JAVA_HOME=/usr/java/jdk-21.0.7' >> /etc/profile
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
RUN echo 'export JAVA_HOME' >> /etc/profile
RUN echo 'export JRE_HOME' >> /etc/profile
RUN echo 'export PATH' >> /etc/profile
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 1
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 1
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-21.0.7/bin/java" 1
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-21.0.7/bin/javac" 1
RUN wget -O docker-entrypoint.sh https://raw.githubusercontent.com/supertokens/supertokens-docker-postgresql/master/docker-entrypoint.sh
@ -51,7 +53,7 @@ RUN set -x \
&& gpgconf --kill all \
&& rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc \
&& chmod +x /usr/local/bin/gosu \
&& wget -O jre.zip "https://raw.githubusercontent.com/supertokens/jre/master/jre-15.0.1-linux.zip" \
&& wget -O jre.zip "https://raw.githubusercontent.com/supertokens/jre/master/jre-21.0.7-linux.zip" \
&& mkdir -p /usr/lib/supertokens/jre \
&& unzip jre.zip \
&& mv jre-*/* /usr/lib/supertokens/jre \

View File

@ -0,0 +1,56 @@
import json
import os
import http.client
def register_core_version(supertokens_api_key, core_version, plugin_interface_array, core_driver_array):
print("Core Version: ", core_version)
print("Plugin Interface Array: ", plugin_interface_array)
print("Core Driver Array: ", core_driver_array)
conn = http.client.HTTPSConnection("api.supertokens.io")
payload = {
"password": supertokens_api_key,
"planType": "FREE",
"version": core_version,
"pluginInterfaces": plugin_interface_array,
"coreDriverInterfaces": core_driver_array
}
headers = {
'Content-Type': 'application/json',
'api-version': '0'
}
conn.request("PUT", "/0/core", json.dumps(payload), headers)
response = conn.getresponse()
if response.status != 200:
print(f"failed core PUT API status code: {response.status}. Exiting!")
exit(1)
conn.close()
def read_core_version():
with open('build.gradle', 'r') as file:
for line in file:
if 'version =' in line:
return line.split('=')[1].strip().strip("'\"")
raise Exception("Could not find version in build.gradle")
core_version = read_core_version()
with open('pluginInterfaceSupported.json', 'r') as fd:
plugin_interface_array = json.load(fd)['versions']
with open('coreDriverInterfaceSupported.json', 'r') as fd:
core_driver_array = json.load(fd)['versions']
register_core_version(
supertokens_api_key=os.environ.get("SUPERTOKENS_API_KEY"),
core_version=core_version,
plugin_interface_array=plugin_interface_array,
core_driver_array=core_driver_array
)

View File

@ -0,0 +1,68 @@
import json
import os
import subprocess
import http.client
def register_plugin_version(supertokens_api_key, plugin_version, plugin_interface_array, plugin_name):
print("Plugin Version: ", plugin_version)
print("Plugin Interface Array: ", plugin_interface_array)
print("Plugin Name: ", plugin_name)
conn = http.client.HTTPSConnection("api.supertokens.io")
payload = {
"password": supertokens_api_key,
"planType": "FREE",
"version": plugin_version,
"pluginInterfaces": plugin_interface_array,
"name": plugin_name
}
headers = {
'Content-Type': 'application/json',
'api-version': '0'
}
conn.request("PUT", "/0/plugin", json.dumps(payload), headers)
response = conn.getresponse()
if response.status != 200:
print(f"failed plugin PUT API status code: {response.status}. Exiting!")
print(f"response: {str(response.read())}")
exit(1)
conn.close()
def read_plugin_version():
with open('build.gradle', 'r') as file:
for line in file:
if 'version =' in line:
return line.split('=')[1].strip().strip("'\"")
raise Exception("Could not find version in build.gradle")
plugin_version = read_plugin_version()
with open('pluginInterfaceSupported.json', 'r') as fd:
plugin_interface_array = json.load(fd)['versions']
def check_if_tag_exists(tag):
try:
result = subprocess.run(['git', 'tag', '-l', tag], capture_output=True, text=True)
return tag in result.stdout
except subprocess.CalledProcessError:
print(f"Error checking for tag {tag}")
return False
dev_tag = f"dev-v{plugin_version}"
if not check_if_tag_exists(dev_tag):
print(f"Tag {dev_tag} does not exist. Exiting!")
exit(0)
register_plugin_version(
supertokens_api_key=os.environ.get("SUPERTOKENS_API_KEY"),
plugin_version=plugin_version,
plugin_interface_array=plugin_interface_array,
plugin_name=os.environ.get("PLUGIN_NAME")
)

39
.github/helpers/release-docker.sh vendored Normal file
View File

@ -0,0 +1,39 @@
#!/bin/bash
set -e
# Check for required arguments
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <source-image:tag> <target-image:tag>"
exit 1
fi
SOURCE_IMAGE="$1"
TARGET_IMAGE="$2"
# Platforms to support
PLATFORMS=("linux/amd64" "linux/arm64")
TEMP_IMAGES=()
# Pull, retag, and push platform-specific images
for PLATFORM in "${PLATFORMS[@]}"; do
ARCH=$(echo $PLATFORM | cut -d'/' -f2)
TEMP_TAG="${TARGET_IMAGE}-${ARCH}"
TEMP_IMAGES+=("$TEMP_TAG")
echo "Pulling $SOURCE_IMAGE for $PLATFORM..."
docker pull --platform $PLATFORM "$SOURCE_IMAGE"
echo "Tagging as $TEMP_TAG..."
docker tag "$SOURCE_IMAGE" "$TEMP_TAG"
echo "Pushing $TEMP_TAG..."
docker push "$TEMP_TAG"
done
# Create and push manifest for multi-arch image
echo "Creating and pushing multi-arch manifest for $TARGET_IMAGE..."
docker manifest create "$TARGET_IMAGE" "${TEMP_IMAGES[@]}"
docker manifest push "$TARGET_IMAGE"
echo "✅ Multi-arch image pushed as $TARGET_IMAGE"

55
.github/helpers/wait-for-docker.py vendored Normal file
View File

@ -0,0 +1,55 @@
import http.client
import json
import time
import os
import sys
REPO = "supertokens/supertokens-core"
SHA = os.environ.get("GITHUB_SHA")
NAME = os.environ.get("WORKFLOW_NAME", "Publish Dev Docker Image")
st = time.time()
def get_latest_actions():
conn = http.client.HTTPSConnection("api.github.com")
url = f"/repos/{REPO}/actions/runs"
headers = {"User-Agent": "Python-http.client"}
conn.request("GET", url, headers=headers)
response = conn.getresponse()
if response.status == 200:
data = response.read()
runs = json.loads(data)['workflow_runs']
found = False
for run in runs:
if run['head_sha'] == SHA and run['name'] == NAME:
found = True
break
if not found:
print("No matching workflow run found.")
sys.exit(1)
if run["status"] == "completed":
if run["conclusion"] == "success":
print("Workflow completed successfully.")
return True
else:
print(f"Workflow failed with conclusion: {run['conclusion']}")
sys.exit(1)
else:
print(f"Failed to fetch workflow runs: {response.status} {response.reason}")
sys.exit(1)
return False
time.sleep(30) # Wait for 30 seconds before checking
while not get_latest_actions():
print("Waiting for the latest actions to complete...")
time.sleep(10)
if time.time() - st > 600:
print("Timed out waiting for the latest actions.")
sys.exit(1)

107
.github/workflows/add-dev-tag.yml vendored Normal file
View File

@ -0,0 +1,107 @@
name: Add dev tags for release
on:
workflow_dispatch:
inputs:
core-version:
description: 'Core version'
required: true
type: string
plugin-interface-version:
description: 'Plugin interface version'
required: true
type: string
new-release-for-plugin-interface:
description: 'New release for plugin interface'
required: true
type: boolean
postgresql-plugin-version:
description: 'Postgres plugin version'
required: true
new-release-for-postgresql-plugin:
description: 'New release for postgres plugin'
required: true
type: boolean
jobs:
dependency-branches:
name: Dependency Branches
environment: publish
runs-on: ubuntu-latest
outputs:
branches: ${{ steps.result.outputs.branches }}
steps:
- uses: actions/checkout@v4
- uses: supertokens/get-core-dependencies-action@main
id: result
with:
run-for: add-dev-tag
core-version: ${{ github.event.inputs.core-version }}
plugin-interface-version: ${{ github.event.inputs.plugin-interface-version }}
postgresql-plugin-version: ${{ github.event.inputs.postgresql-plugin-version }}
add-dev-tag:
environment: publish
runs-on: ubuntu-latest
needs: dependency-branches
steps:
- name: Set up JDK 21.0.7
uses: actions/setup-java@v2
with:
java-version: 21.0.7
distribution: zulu
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-root
path: ./supertokens-root
ref: master
- name: Checkout supertokens-core
run: |
cd supertokens-root
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-core.git
cd supertokens-core
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['core'] }}
- name: Checkout supertokens-plugin-interface
run: |
cd supertokens-root
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-plugin-interface.git
cd supertokens-plugin-interface
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['plugin-interface'] }}
- name: Checkout supertokens-postgresql-plugin
run: |
cd supertokens-root
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-postgresql-plugin.git
cd supertokens-postgresql-plugin
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['postgresql'] }}
- name: Load Modules
run: |
cd supertokens-root
echo "core,master
plugin-interface,master
postgresql-plugin,master
" > modules.txt
cat modules.txt
./loadModules
- name: Setup test env
run: cd supertokens-root && ./utils/setupTestEnv --local
- name: Git config
run: |
git config --global user.name "Supertokens Bot"
git config --global user.email "<>"
- name: Add dev tag to plugin interface
if: ${{ github.event.inputs.new-release-for-plugin-interface == 'true' }}
run: |
echo "Adding dev tag to plugin interface"
cd supertokens-root/supertokens-plugin-interface
./addDevTag
- name: Add dev tag to postgres plugin
if: ${{ github.event.inputs.new-release-for-postgresql-plugin == 'true' }}
run: |
echo "Adding dev tag to postgres plugin"
cd supertokens-root/supertokens-postgresql-plugin
./addDevTag
- name: Add dev tag to core
run: |
echo "Adding dev tag to core"
cd supertokens-root/supertokens-core
./addDevTag

134
.github/workflows/container-check.yml vendored Normal file
View File

@ -0,0 +1,134 @@
name: Container Security Scan
on:
# Allow manual triggering
workflow_dispatch:
# Run automatically once a day at 2 AM UTC
schedule:
- cron: '0 2 * * *'
jobs:
container-scan:
name: Scan SuperTokens PostgreSQL Container
runs-on: ubuntu-latest
steps:
- name: Run Azure Container Scan
id: container-scan
uses: Azure/container-scan@v0
continue-on-error: true
with:
image-name: supertokens/supertokens-postgresql:latest
severity-threshold: LOW
run-quality-checks: false
env:
DOCKER_CONTENT_TRUST: 1
- name: Upload scan results
id: upload-scan-results
uses: actions/upload-artifact@v4
with:
name: container-scan-results
path: |
${{ steps.container-scan.outputs.scan-report-path }}
retention-days: 30
- name: Generate Security Summary
id: security-summary
run: |
echo "summary<<EOF" >> $GITHUB_OUTPUT
echo "**Image:** \`supertokens/supertokens-postgresql:latest\`\n" >> $GITHUB_OUTPUT
echo "**Scan Date:** \`$(date -u)\`\n" >> $GITHUB_OUTPUT
echo "\n" >> $GITHUB_OUTPUT
# Get the scan report path from the container scan output
SCAN_REPORT_PATH="${{ steps.container-scan.outputs.scan-report-path }}"
if [ -f "$SCAN_REPORT_PATH" ]; then
# Count vulnerabilities by severity using the correct JSON structure
critical=$(jq '[.vulnerabilities[]? | select(.severity == "CRITICAL")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
high=$(jq '[.vulnerabilities[]? | select(.severity == "HIGH")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
medium=$(jq '[.vulnerabilities[]? | select(.severity == "MEDIUM")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
low=$(jq '[.vulnerabilities[]? | select(.severity == "LOW")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
total_vulns=$(jq '[.vulnerabilities[]?] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
echo "**Total Vulnerabilities:** $total_vulns\n" >> $GITHUB_OUTPUT
echo "\n" >> $GITHUB_OUTPUT
echo "- 🔴 **Critical**: $critical\n" >> $GITHUB_OUTPUT
echo "- 🟠 **High**: $high\n" >> $GITHUB_OUTPUT
echo "- 🟡 **Medium**: $medium\n" >> $GITHUB_OUTPUT
echo "- 🟢 **Low**: $low\n" >> $GITHUB_OUTPUT
echo "\n" >> $GITHUB_OUTPUT
else
echo "❌ **Scan results not found or scan failed**" >> $GITHUB_OUTPUT
fi
echo "\n" >> $GITHUB_OUTPUT
echo "[📃 Download the full report](${{ steps.upload-scan-results.outputs.artifact-url }})\n" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Add to Action Summary
run: |
echo "**Image:** \`supertokens/supertokens-postgresql:latest\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Scan Date:** \`$(date -u)\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Get the scan report path from the container scan output
SCAN_REPORT_PATH="${{ steps.container-scan.outputs.scan-report-path }}"
if [ -f "$SCAN_REPORT_PATH" ]; then
# Count vulnerabilities by severity using the correct JSON structure
critical=$(jq '[.vulnerabilities[]? | select(.severity == "CRITICAL")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
high=$(jq '[.vulnerabilities[]? | select(.severity == "HIGH")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
medium=$(jq '[.vulnerabilities[]? | select(.severity == "MEDIUM")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
low=$(jq '[.vulnerabilities[]? | select(.severity == "LOW")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
total_vulns=$(jq '[.vulnerabilities[]?] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
echo "**Total Vulnerabilities:** $total_vulns" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- 🔴 **Critical**: $critical" >> $GITHUB_STEP_SUMMARY
echo "- 🟠 **High**: $high" >> $GITHUB_STEP_SUMMARY
echo "- 🟡 **Medium**: $medium" >> $GITHUB_STEP_SUMMARY
echo "- 🟢 **Low**: $low" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Vulnerabilities:**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| ID | Package | Severity | | Description |" >> $GITHUB_STEP_SUMMARY
echo "|----|---------|----------|-|-------------|" >> $GITHUB_STEP_SUMMARY
# Extract and format vulnerabilities into a table with colored severity indicators, excluding LOW severity
jq -r '.vulnerabilities[]? | select(.severity != "LOW") | "| \(.vulnerabilityId // "N/A") | \(.packageName // "N/A") | \(.severity // "UNKNOWN") | \(if .severity == "CRITICAL" then "🔴" elif .severity == "HIGH" then "🟠" elif .severity == "MEDIUM" then "🟡" else "🟢" end) | \((.description // "No description available") | gsub("\n"; " ")) |"' "$SCAN_REPORT_PATH" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
else
echo "❌ **Scan results not found or scan failed**" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "[📃 Download the full report](${{ steps.upload-scan-results.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY
- name: Post notification on Slack channel
id: deployment_message
uses: slackapi/slack-github-action@v2.1.0
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: ""
blocks:
- type: "header"
text:
type: "plain_text"
text: "${{ steps.container-scan.outcome == 'success' && '✅' || '❌' }} Vulnerability Report: ${{ steps.container-scan.outcome == 'success' && 'All okay' || 'Needs attention' }}"
- type: "markdown"
text: "${{ steps.security-summary.outputs.summary }}"

153
.github/workflows/dev-tag.yml vendored Normal file
View File

@ -0,0 +1,153 @@
name: Checks for release
on:
push:
branches:
- '[0-9]+.[0-9]+'
tags:
- 'dev-*'
jobs:
dependency-versions:
name: Dependency Versions
runs-on: ubuntu-latest
outputs:
versions: ${{ steps.result.outputs.versions }}
branches: ${{ steps.result.outputs.branches }}
steps:
- uses: actions/checkout@v4
- uses: supertokens/get-core-dependencies-action@main
with:
run-for: PR
id: result
new-core-version:
environment: publish
name: New core version
runs-on: ubuntu-latest
needs: [dependency-versions]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Run script
env:
SUPERTOKENS_API_KEY: ${{ secrets.SUPERTOKENS_API_KEY }}
run: |
python .github/helpers/register-new-core-version.py
new-plugin-versions:
environment: publish
name: New plugin versions
runs-on: ubuntu-latest
needs: [dependency-versions]
strategy:
fail-fast: false
matrix:
plugin:
- postgresql
# no longer supported
# - mysql
# - mongodb
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v4
with:
path: ./supertokens-plugin
repository: supertokens/supertokens-${{ matrix.plugin }}-plugin
ref: ${{ fromJson(needs.dependency-versions.outputs.branches)[matrix.plugin] }}
fetch-depth: 0
fetch-tags: true
- name: Run script
env:
SUPERTOKENS_API_KEY: ${{ secrets.SUPERTOKENS_API_KEY }}
PLUGIN_NAME: ${{ matrix.plugin }}
run: |
cd supertokens-plugin
python ../.github/helpers/register-new-plugin-version.py
unit-tests:
name: Run unit tests
needs: [new-core-version, new-plugin-versions]
uses: ./.github/workflows/unit-test.yml
wait-for-docker:
name: Wait for Docker
runs-on: ubuntu-latest
needs: [new-core-version, new-plugin-versions]
outputs:
tag: ${{ steps.set_tag.outputs.TAG }}
steps:
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Checkout
uses: actions/checkout@v4
- name: Wait for Docker build
env:
SHA: ${{ github.sha }}
run: |
python .github/helpers/wait-for-docker.py
- name: set tag
id: set_tag
run: |
echo "TAG=${GITHUB_REF}" | sed 's/refs\/heads\///g' | sed 's/\//_/g' >> $GITHUB_OUTPUT
stress-tests:
needs: [wait-for-docker]
uses: ./.github/workflows/stress-tests.yml
with:
tag: ${{ needs.wait-for-docker.outputs.tag }}
mark-as-passed:
environment: publish
needs: [dependency-versions, unit-tests, stress-tests]
name: Mark as passed
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
plugin:
- sqlite
- postgresql
# no longer supported
# - mysql
# - mongodb
steps:
- name: Mark plugin as passed
if: matrix.plugin != 'sqlite' && fromJson(needs.dependency-versions.outputs.versions)[matrix.plugin] != ''
uses: muhfaris/request-action@main
with:
url: https://api.supertokens.io/0/plugin
method: PATCH
headers: |
{
"Content-Type": "application/json",
"api-version": "0"
}
body: |
{
"password": "${{ secrets.SUPERTOKENS_API_KEY }}",
"version": "${{ fromJson(needs.dependency-versions.outputs.versions)[matrix.plugin] }}",
"planType": "FREE",
"name": "${{ matrix.plugin }}",
"testPassed": true
}
- name: Mark core as passed
if: matrix.plugin == 'sqlite' && fromJson(needs.dependency-versions.outputs.versions)['core'] != ''
uses: muhfaris/request-action@main
with:
url: https://api.supertokens.io/0/core
method: PATCH
headers: |
{
"Content-Type": "application/json",
"api-version": "0"
}
body: |
{
"password": "${{ secrets.SUPERTOKENS_API_KEY }}",
"version": "${{ fromJson(needs.dependency-versions.outputs.versions)['core'] }}",
"planType": "FREE",
"testPassed": true
}

148
.github/workflows/do-release.yml vendored Normal file
View File

@ -0,0 +1,148 @@
name: Do Release
on:
workflow_dispatch:
inputs:
core-version:
description: 'Core version'
required: true
type: string
plugin-interface-version:
description: 'Plugin interface version'
required: true
type: string
new-release-for-plugin-interface:
description: 'New release for plugin interface'
required: true
type: boolean
postgresql-plugin-version:
description: 'Postgres plugin version'
required: true
new-release-for-postgresql-plugin:
description: 'New release for postgres plugin'
required: true
type: boolean
is-latest-release:
description: 'Is this the latest release?'
required: true
type: boolean
jobs:
dependency-branches:
name: Dependency Branches
environment: publish
runs-on: ubuntu-latest
outputs:
branches: ${{ steps.result.outputs.branches }}
versions: ${{ steps.result.outputs.versions }}
steps:
- uses: actions/checkout@v4
- uses: supertokens/get-core-dependencies-action@main
id: result
with:
run-for: add-dev-tag
core-version: ${{ github.event.inputs.core-version }}
plugin-interface-version: ${{ github.event.inputs.plugin-interface-version }}
postgresql-plugin-version: ${{ github.event.inputs.postgresql-plugin-version }}
release-docker:
environment: publish
name: Release Docker
runs-on: ubuntu-latest
needs: dependency-branches
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up JDK 21.0.7
uses: actions/setup-java@v2
with:
java-version: 21.0.7
distribution: zulu
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Tag and Push Docker Image
run: |
tag=${{ github.event.inputs.core-version }}
major=$(echo $tag | cut -d. -f1)
minor=$(echo $tag | cut -d. -f1,2)
bash .github/helpers/release-docker.sh supertokens/supertokens-dev-postgresql:$minor supertokens/supertokens-postgresql:$major
bash .github/helpers/release-docker.sh supertokens/supertokens-dev-postgresql:$minor supertokens/supertokens-postgresql:$minor
bash .github/helpers/release-docker.sh supertokens/supertokens-dev-postgresql:$minor supertokens/supertokens-postgresql:$tag
if [ "${{ github.event.inputs.is-latest-release }}" == "true" ]; then
bash .github/helpers/release-docker.sh supertokens/supertokens-dev-postgresql:$minor supertokens/supertokens-postgresql:latest
fi
add-release-tag:
environment: publish
runs-on: ubuntu-latest
needs: [dependency-branches, release-docker]
steps:
- name: Set up JDK 21.0.7
uses: actions/setup-java@v2
with:
java-version: 21.0.7
distribution: zulu
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-root
path: ./supertokens-root
ref: master
- name: Checkout supertokens-core
run: |
cd supertokens-root
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-core.git
cd supertokens-core
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['core'] }}
- name: Checkout supertokens-plugin-interface
run: |
cd supertokens-root
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-plugin-interface.git
cd supertokens-plugin-interface
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['plugin-interface'] }}
- name: Checkout supertokens-postgresql-plugin
run: |
cd supertokens-root
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-postgresql-plugin.git
cd supertokens-postgresql-plugin
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['postgresql'] }}
- name: Add release password
run: |
cd supertokens-root
echo "${{ secrets.SUPERTOKENS_API_KEY }}" > releasePassword
echo "${{ secrets.SUPERTOKENS_API_KEY }}" > apiPassword
- name: Load Modules
run: |
cd supertokens-root
echo "core,master
plugin-interface,master
postgresql-plugin,master
" > modules.txt
cat modules.txt
./loadModules
- name: Setup test env
run: cd supertokens-root && ./utils/setupTestEnv --local
- name: Git config
run: |
git config --global user.name "Supertokens Bot"
git config --global user.email "<>"
- name: Add release tag to plugin interface
if: ${{ github.event.inputs.new-release-for-plugin-interface == 'true' }}
run: |
echo "Adding release tag to plugin interface"
cd supertokens-root/supertokens-plugin-interface
./addReleaseTag
- name: Add release tag to postgres plugin
if: ${{ github.event.inputs.new-release-for-postgresql-plugin == 'true' }}
run: |
echo "Adding release tag to postgres plugin"
cd supertokens-root/supertokens-postgresql-plugin
./addReleaseTag
- name: Add release tag to core
run: |
echo "Adding release tag to core"
cd supertokens-root/supertokens-core
./addReleaseTag

View File

@ -1,15 +0,0 @@
name: "Enforcing changelog in PRs Workflow"
on:
pull_request:
types: [ opened, synchronize, reopened, ready_for_review, labeled, unlabeled ]
jobs:
# Enforces the update of a changelog file on every pull request
changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dangoslen/changelog-enforcer@v2
with:
changeLogPath: 'CHANGELOG.md'
skipLabels: 'Skip-Changelog'

View File

@ -1,20 +0,0 @@
name: "Lint PR Title"
on:
pull_request:
types:
- opened
- reopened
- edited
- synchronize
jobs:
pr-title:
name: Lint PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
validateSingleCommit: true

27
.github/workflows/pr-checks.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: PR Checks
on:
pull_request:
types: [ opened, synchronize, reopened, ready_for_review, labeled, unlabeled ]
jobs:
pr-title:
name: Lint PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
validateSingleCommit: true
changelog:
name: Enforce Changelog
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dangoslen/changelog-enforcer@v2
with:
changeLogPath: 'CHANGELOG.md'
skipLabels: 'Skip-Changelog'
unit-tests:
name: Run unit tests
uses: ./.github/workflows/unit-test.yml

View File

@ -3,10 +3,82 @@ on:
push:
branches:
- "**"
tags:
- 'dev-*'
jobs:
docker:
dependency-branches:
name: Dependency Branches
runs-on: ubuntu-latest
outputs:
branches: ${{ steps.result.outputs.branches }}
steps:
- uses: actions/checkout@v4
- uses: supertokens/get-core-dependencies-action@main
id: result
with:
run-for: PR
core-branch: ${{ github.ref_name }}
docker:
name: Docker
runs-on: ubuntu-latest
needs: dependency-branches
outputs:
tag: ${{ steps.set_tag.outputs.TAG }}
strategy:
fail-fast: false
matrix:
plugin:
- postgresql
# no longer supported
# - mysql
# - mongodb
steps:
- name: Set up JDK 21.0.7
uses: actions/setup-java@v2
with:
java-version: 21.0.7
distribution: zulu
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-root
path: ./supertokens-root
ref: master
- uses: actions/checkout@v2
with:
path: ./supertokens-root/supertokens-core
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-plugin-interface
path: ./supertokens-root/supertokens-plugin-interface
ref: ${{ fromJson(needs.dependency-branches.outputs.branches)['plugin-interface'] }}
- uses: actions/checkout@v2
if: matrix.plugin != 'sqlite'
with:
repository: supertokens/supertokens-${{ matrix.plugin }}-plugin
path: ./supertokens-root/supertokens-${{ matrix.plugin }}-plugin
ref: ${{ fromJson(needs.dependency-branches.outputs.branches)[matrix.plugin] }}
- name: Load Modules
run: |
cd supertokens-root
echo "core,master
plugin-interface,master
${{ matrix.plugin }}-plugin,master
" > modules.txt
cat modules.txt
./loadModules
- name: Setup test env
run: cd supertokens-root && ./utils/setupTestEnv --local
- name: Generate config file
run: |
cd supertokens-root
touch config_temp.yaml
cat supertokens-core/config.yaml >> config_temp.yaml
cat supertokens-${{ matrix.plugin }}-plugin/config.yaml >> config_temp.yaml
mv config_temp.yaml config.yaml
- name: set tag
id: set_tag
run: |
@ -17,26 +89,16 @@ jobs:
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Set up QEMU
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# - name: Build and export to Docker
# uses: docker/build-push-action@v6
# with:
# load: true
# tags: ${{ env.TEST_TAG }}
# - name: Test
# run: |
# docker run --rm ${{ env.TEST_TAG }}
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: supertokens/supertokens-core:dev-branch-${{ steps.set_tag.outputs.TAG }}
file: .github/helpers/Dockerfile
context: ./supertokens-root
tags: supertokens/supertokens-dev-${{ matrix.plugin }}:${{ steps.set_tag.outputs.TAG }}
file: ./supertokens-root/supertokens-${{ matrix.plugin }}-plugin/.github/helpers/docker/Dockerfile
platforms: linux/amd64,linux/arm64

47
.github/workflows/stress-tests.yml vendored Normal file
View File

@ -0,0 +1,47 @@
name: Stress Tests
on:
workflow_call:
inputs:
tag:
description: 'Docker image tag to use'
required: true
type: string
jobs:
stress-tests:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: |
cd stress-tests
npm install
- name: Update Docker image in compose
run: |
cd stress-tests
sed -i 's|supertokens/supertokens-postgresql|supertokens/supertokens-dev-postgresql:${{ inputs.tag }}|' docker-compose.yml
cat docker-compose.yml
- name: Bring up the services
run: |
cd stress-tests
docker compose up -d
- name: Generate user jsons
run: |
cd stress-tests
npm run generate-users
- name: Run one million users test
id: one-million-users
run: |
cd stress-tests
npm run one-million-users | tee stress-tests.log
- name: Display Test Statistics
run: |
echo "## Stress Test Results" >> $GITHUB_STEP_SUMMARY
echo "| Test | Duration |" >> $GITHUB_STEP_SUMMARY
echo "|------|----------|" >> $GITHUB_STEP_SUMMARY
jq -r '.measurements[] | "| \(.title) | \(.formatted) |"' stress-tests/stats.json >> $GITHUB_STEP_SUMMARY

View File

@ -1,24 +0,0 @@
name: "Check if \"Run tests\" action succeeded"
on:
pull_request:
types:
- opened
- reopened
- edited
- synchronize
jobs:
pr-run-test-action:
name: Check if "Run tests" action succeeded
timeout-minutes: 60
concurrency:
group: ${{ github.head_ref }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: node install
run: cd ./.github/helpers && npm i
- name: Calling github API
run: cd ./.github/helpers && GITHUB_TOKEN=${{ github.token }} REPO=${{ github.repository }} RUN_ID=${{ github.run_id }} BRANCH=${{ github.head_ref }} JOB_ID=${{ github.job }} SOURCE_OWNER=${{ github.event.pull_request.head.repo.owner.login }} CURRENT_SHA=${{ github.event.pull_request.head.sha }} node node_modules/github-workflow-helpers/test-pass-check-pr.js

View File

@ -1,37 +0,0 @@
name: "Run tests"
on:
workflow_dispatch:
inputs:
pluginRepoOwnerName:
description: 'supertokens-plugin-interface repo owner name'
default: supertokens
required: true
pluginInterfaceBranch:
description: 'supertokens-plugin-interface repos branch name'
default: master
required: true
jobs:
test_job:
name: Run tests
timeout-minutes: 60
runs-on: ubuntu-latest
container: rishabhpoddar/supertokens_core_testing
steps:
- uses: actions/checkout@v2
- name: Cloning supertokens-root
run: cd ../ && git clone https://github.com/supertokens/supertokens-root.git
- name: Update Java 1
run: update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 2
- name: Update Java 2
run: update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 2
- name: Modifying modules.txt in supertokens-root
run: cd ../supertokens-root && echo "core,master\nplugin-interface,${{ github.event.inputs.pluginInterfaceBranch }},${{ github.event.inputs.pluginRepoOwnerName }}" > modules.txt
- name: Contents of modules.txt
run: cat ../supertokens-root/modules.txt
- name: Running loadModules in supertokens-root
run: cd ../supertokens-root && ./loadModules
- name: Copying current supertokens-core branch into supertokens-root
run: cd ../supertokens-root && rm -rf ./supertokens-core && cp -r ../supertokens-core ./
- name: Building and running tests
run: cd ../supertokens-root && ./startTestingEnv

87
.github/workflows/unit-test.yml vendored Normal file
View File

@ -0,0 +1,87 @@
name: Unit Tests
on:
workflow_call:
jobs:
dependency-branches:
name: Dependency Branches
runs-on: ubuntu-22.04
outputs:
branches: ${{ steps.result.outputs.branches }}
steps:
- uses: actions/checkout@v4
- uses: supertokens/get-core-dependencies-action@main
id: result
with:
run-for: PR
core-branch: ${{ github.head_ref }}
test:
name: Unit tests
needs: dependency-branches
strategy:
fail-fast: false
matrix:
plugin:
- sqlite
- postgresql
# no longer supported
# - mysql
# - mongodb
runs-on: ubuntu-22.04
steps:
- name: Set up JDK 21.0.7
uses: actions/setup-java@v2
with:
java-version: 21.0.7
distribution: zulu
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-root
path: ./supertokens-root
ref: master
- uses: actions/checkout@v2
with:
path: ./supertokens-root/supertokens-core
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-plugin-interface
path: ./supertokens-root/supertokens-plugin-interface
ref: ${{ fromJson(needs.dependency-branches.outputs.branches)['plugin-interface'] }}
- uses: actions/checkout@v2
if: matrix.plugin != 'sqlite'
with:
repository: supertokens/supertokens-${{ matrix.plugin }}-plugin
path: ./supertokens-root/supertokens-${{ matrix.plugin }}-plugin
ref: ${{ fromJson(needs.dependency-branches.outputs.branches)[matrix.plugin] }}
- name: Load Modules
run: |
cd supertokens-root
echo "core,master
plugin-interface,master
${{ matrix.plugin }}-plugin,master
" > modules.txt
cat modules.txt
./loadModules
- name: Setup test env
run: cd supertokens-root && ./utils/setupTestEnv --local
- name: Start ${{ matrix.plugin }} server
if: matrix.plugin != 'sqlite'
run: cd supertokens-root/supertokens-${{ matrix.plugin }}-plugin && ./startDb.sh
- name: Run tests
env:
ST_PLUGIN_NAME: ${{ matrix.plugin }}
run: |
cd supertokens-root
./gradlew test
- name: Publish Test Report
uses: mikepenz/action-junit-report@v5
if: always()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
detailed_summary: true
include_passed: false
annotate_notice: true

6
.gitignore vendored
View File

@ -12,6 +12,7 @@ gradle-app.setting
!cli/jar/**/*.jar
!downloader/jar/**/*.jar
!ee/jar/**/*.jar
!src/main/resources/**/*.jar
*target*
*.war
@ -47,4 +48,7 @@ local.properties
*.iml
ee/bin
addDevTag
addReleaseTag
addReleaseTag
install-linux.sh
install-windows.bat

View File

@ -7,6 +7,341 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [11.3.0]
- Adds SAML features
- Fixes potential deadlock issue with `TelemetryProvider`
- Adds DeadlockLogger as an utility for discovering deadlock issues
### Migration
```sql
CREATE TABLE IF NOT EXISTS saml_clients (
app_id VARCHAR(64) NOT NULL DEFAULT 'public',
tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',
client_id VARCHAR(256) NOT NULL,
client_secret TEXT,
sso_login_url TEXT NOT NULL,
redirect_uris TEXT NOT NULL,
default_redirect_uri TEXT NOT NULL,
idp_entity_id VARCHAR(256) NOT NULL,
idp_signing_certificate TEXT NOT NULL,
allow_idp_initiated_login BOOLEAN NOT NULL DEFAULT FALSE,
enable_request_signing BOOLEAN NOT NULL DEFAULT FALSE,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
CONSTRAINT saml_clients_pkey PRIMARY KEY(app_id, tenant_id, client_id),
CONSTRAINT saml_clients_idp_entity_id_key UNIQUE (app_id, tenant_id, idp_entity_id),
CONSTRAINT saml_clients_app_id_fkey FOREIGN KEY(app_id) REFERENCES apps (app_id) ON DELETE CASCADE,
CONSTRAINT saml_clients_tenant_id_fkey FOREIGN KEY(app_id, tenant_id) REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS saml_clients_app_id_tenant_id_index ON saml_clients (app_id, tenant_id);
CREATE TABLE IF NOT EXISTS saml_relay_state (
app_id VARCHAR(64) NOT NULL DEFAULT 'public',
tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',
relay_state VARCHAR(256) NOT NULL,
client_id VARCHAR(256) NOT NULL,
state TEXT NOT NULL,
redirect_uri TEXT NOT NULL,
created_at BIGINT NOT NULL,
CONSTRAINT saml_relay_state_pkey PRIMARY KEY(app_id, tenant_id, relay_state),
CONSTRAINT saml_relay_state_app_id_fkey FOREIGN KEY(app_id) REFERENCES apps (app_id) ON DELETE CASCADE,
CONSTRAINT saml_relay_state_tenant_id_fkey FOREIGN KEY(app_id, tenant_id) REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS saml_relay_state_app_id_tenant_id_index ON saml_relay_state (app_id, tenant_id);
CREATE INDEX IF NOT EXISTS saml_relay_state_expires_at_index ON saml_relay_state (expires_at);
CREATE TABLE IF NOT EXISTS saml_claims (
app_id VARCHAR(64) NOT NULL DEFAULT 'public',
tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',
client_id VARCHAR(256) NOT NULL,
code VARCHAR(256) NOT NULL,
claims TEXT NOT NULL,
created_at BIGINT NOT NULL,
CONSTRAINT saml_claims_pkey PRIMARY KEY(app_id, tenant_id, code),
CONSTRAINT saml_claims_app_id_fkey FOREIGN KEY(app_id) REFERENCES apps (app_id) ON DELETE CASCADE,
CONSTRAINT saml_claims_tenant_id_fkey FOREIGN KEY(app_id, tenant_id) REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS saml_claims_app_id_tenant_id_index ON saml_claims (app_id, tenant_id);
CREATE INDEX IF NOT EXISTS saml_claims_expires_at_index ON saml_claims (expires_at);
```
## [11.2.1]
- Fixes deadlock issue with `ResourceDistributor`
- Fixes race issues with Refreshing OAuth token
## [11.2.0]
- Adds opentelemetry-javaagent to the core distribution
## [11.1.1]
- Updates tomcat-embed to 11.0.12 because of security vulnerabilities
## [11.1.0]
- Adds hikari logs to opentelemetry
- Fetches core and plugin config from env
- Open Telemetry configuration is now optional
- Migrates API calls from supertokens.io to supertokens.com
## [11.0.5]
- Adds all logs to telemetry which were logged with `io/supertokens/output/Logging.java`
- Upgrades the embedded tomcat to 11.0.8 because of security vulnerabilities
- Adds back previously removed `implementationDependencies.json`, but now it is generated by the build process
## [11.0.4]
- Fixes user to roles association in bulk import users when the user is not a primary user
## [11.0.3]
- Fixes BatchUpdateException checks and error handling to prevent bulk import users stuck in `PROCESSING` state
- Adds more DEBUG logging to the bulk import users process
## [11.0.2]
- Fixes `AuthRecipe#getUserByAccountInfo` to consider the tenantId instead of the appId when fetching the webauthn user
## [11.0.1]
- Upgrades the embedded tomcat 11.0.6 and logback classic to 1.5.13 because of security vulnerabilities
## [11.0.0]
- Migrates tests to Github Actions
- Updates JRE to 21.
## [10.1.4]
- Fixes bulk migration user roles association when there is no external userId assigned to the user
- Bulk migration now actually uses the `isVerified` field's value in the loginMethod input
- Fixes nullpointer exception in bulk migration error handling in case of null external user id
## [10.1.3]
- Version bumped for re-release
## [10.1.2]
- Adds user_id index to the user roles table
- Adds more debug logging to bulk migration
- Adds more tests to bulk migration
### Migration
If using PostgreSQL, run the following SQL script:
```sql
CREATE INDEX IF NOT EXISTS user_roles_app_id_user_id_index ON user_roles (app_id, user_id);
```
If using MySQL, run the following SQL script:
```sql
CREATE INDEX user_roles_app_id_user_id_index ON user_roles (app_id, user_id);
```
## [10.1.1]
- Adds debug logging for the bulk migration process
- Bulk migration users upload now returns the ids of the users.
- Bulk Migration now requires Account Linking to be enabled only if the input data justifies it
- Speed up Bulk Migration's account linking and primary user making
## [10.1.0]
- Adds Webauthn (Passkeys) support to core
- Adds APIs:
- GET `/recipe/webauthn/user/credential/`
- GET `/recipe/webauthn/user/credential/list`
- GET `/recipe/webauthn/options`
- GET `/recipe/webauthn/user/recover`
- POST `/recipe/webauthn/options/register`
- POST `/recipe/webauthn/options/signin`
- POST `/recipe/webauthn/user/credential/register`
- POST `/recipe/webauthn/signup`
- POST `/recipe/webauthn/signin`
- POST `/recipe/webauthn/user/recover/token`
- POST `/recipe/webauthn/user/recover/token/consume`
- PUT `/recipe/webauthn/user/email`
- DELETE `/recipe/webauthn/user/credential/remove`
- DELETE `/recipe/webauthn/options/remove`
- Adds additional indexing for `emailverification_verified_emails`
- Introduces `bulk_migration_batch_size` core config
- Introduces `BULK_MIGRATION_CRON_ENABLED` environment variable to control the bulk migration cron job
### Migration
If using PostgreSQL, run the following SQL script:
```sql
CREATE INDEX IF NOT EXISTS emailverification_verified_emails_app_id_email_index ON emailverification_verified_emails
(app_id, email);
CREATE TABLE IF NOT EXISTS webauthn_account_recovery_tokens (
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL,
user_id CHAR(36) NOT NULL,
email VARCHAR(256) NOT NULL,
token VARCHAR(256) NOT NULL,
expires_at BIGINT NOT NULL,
CONSTRAINT webauthn_account_recovery_token_pkey PRIMARY KEY (app_id, tenant_id, user_id, token),
CONSTRAINT webauthn_account_recovery_token_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES
all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS webauthn_credentials (
id VARCHAR(256) NOT NULL,
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
rp_id VARCHAR(256) NOT NULL,
user_id CHAR(36),
counter BIGINT NOT NULL,
public_key BYTEA NOT NULL,
transports TEXT NOT NULL,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
CONSTRAINT webauthn_credentials_pkey PRIMARY KEY (app_id, rp_id, id),
CONSTRAINT webauthn_credentials_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES webauthn_users
(app_id, user_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS webauthn_generated_options (
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
tenant_id VARCHAR(64) DEFAULT 'public'NOT NULL,
id CHAR(36) NOT NULL,
challenge VARCHAR(256) NOT NULL,
email VARCHAR(256),
rp_id VARCHAR(256) NOT NULL,
rp_name VARCHAR(256) NOT NULL,
origin VARCHAR(256) NOT NULL,
expires_at BIGINT NOT NULL,
created_at BIGINT NOT NULL,
user_presence_required BOOLEAN DEFAULT false NOT NULL,
user_verification VARCHAR(12) DEFAULT 'preferred' NOT NULL,
CONSTRAINT webauthn_generated_options_pkey PRIMARY KEY (app_id, tenant_id, id),
CONSTRAINT webauthn_generated_options_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES tenants
(app_id, tenant_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS webauthn_user_to_tenant (
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL,
user_id CHAR(36) NOT NULL,
email VARCHAR(256) NOT NULL,
CONSTRAINT webauthn_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email),
CONSTRAINT webauthn_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id),
CONSTRAINT webauthn_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES
all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS webauthn_users (
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
user_id CHAR(36) NOT NULL,
email VARCHAR(256) NOT NULL,
rp_id VARCHAR(256) NOT NULL,
time_joined BIGINT NOT NULL,
CONSTRAINT webauthn_users_pkey PRIMARY KEY (app_id, user_id),
CONSTRAINT webauthn_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES app_id_to_user_id(app_id,
user_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS webauthn_user_to_tenant_email_index ON webauthn_user_to_tenant (app_id, email);
CREATE INDEX IF NOT EXISTS webauthn_user_challenges_expires_at_index ON webauthn_generated_options (app_id, tenant_id, expires_at);
CREATE INDEX IF NOT EXISTS webauthn_credentials_user_id_index ON webauthn_credentials (user_id);
CREATE INDEX IF NOT EXISTS webauthn_account_recovery_token_token_index ON webauthn_account_recovery_tokens (app_id, tenant_id, token);
CREATE INDEX IF NOT EXISTS webauthn_account_recovery_token_expires_at_index ON webauthn_account_recovery_tokens (expires_at DESC);
CREATE INDEX IF NOT EXISTS webauthn_account_recovery_token_email_index ON webauthn_account_recovery_tokens (app_id, tenant_id, email);
```
If using MySQL, run the following SQL script:
```sql
CREATE INDEX emailverification_verified_emails_app_id_email_index ON emailverification_verified_emails
(app_id, email);
CREATE TABLE IF NOT EXISTS webauthn_account_recovery_tokens (
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL,
user_id CHAR(36) NOT NULL,
email VARCHAR(256) NOT NULL,
token VARCHAR(256) NOT NULL,
expires_at BIGINT NOT NULL,
CONSTRAINT webauthn_account_recovery_token_pkey PRIMARY KEY (app_id, tenant_id, user_id, token),
CONSTRAINT webauthn_account_recovery_token_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES
all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS webauthn_credentials (
id VARCHAR(256) NOT NULL,
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
rp_id VARCHAR(256) NOT NULL,
user_id CHAR(36),
counter BIGINT NOT NULL,
public_key BLOB NOT NULL,
transports TEXT NOT NULL,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
CONSTRAINT webauthn_credentials_pkey PRIMARY KEY (app_id, rp_id, id),
CONSTRAINT webauthn_credentials_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES webauthn_users
(app_id, user_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS webauthn_generated_options (
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
tenant_id VARCHAR(64) DEFAULT 'public'NOT NULL,
id CHAR(36) NOT NULL,
challenge VARCHAR(256) NOT NULL,
email VARCHAR(256),
rp_id VARCHAR(256) NOT NULL,
rp_name VARCHAR(256) NOT NULL,
origin VARCHAR(256) NOT NULL,
expires_at BIGINT NOT NULL,
created_at BIGINT NOT NULL,
user_presence_required BOOLEAN DEFAULT false NOT NULL,
user_verification VARCHAR(12) DEFAULT 'preferred' NOT NULL,
CONSTRAINT webauthn_generated_options_pkey PRIMARY KEY (app_id, tenant_id, id),
CONSTRAINT webauthn_generated_options_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES tenants
(app_id, tenant_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS webauthn_user_to_tenant (
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL,
user_id CHAR(36) NOT NULL,
email VARCHAR(256) NOT NULL,
CONSTRAINT webauthn_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email),
CONSTRAINT webauthn_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id),
CONSTRAINT webauthn_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES
all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS webauthn_users (
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
user_id CHAR(36) NOT NULL,
email VARCHAR(256) NOT NULL,
rp_id VARCHAR(256) NOT NULL,
time_joined BIGINT NOT NULL,
CONSTRAINT webauthn_users_pkey PRIMARY KEY (app_id, user_id),
CONSTRAINT webauthn_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES app_id_to_user_id (app_id,
user_id) ON DELETE CASCADE
);
CREATE INDEX webauthn_user_to_tenant_email_index ON webauthn_user_to_tenant (app_id, email);
CREATE INDEX webauthn_user_challenges_expires_at_index ON webauthn_generated_options (app_id, tenant_id, expires_at);
CREATE INDEX webauthn_credentials_user_id_index ON webauthn_credentials (user_id);
CREATE INDEX webauthn_account_recovery_token_token_index ON webauthn_account_recovery_tokens (app_id, tenant_id, token);
CREATE INDEX webauthn_account_recovery_token_expires_at_index ON webauthn_account_recovery_tokens (expires_at DESC);
CREATE INDEX webauthn_account_recovery_token_email_index ON webauthn_account_recovery_tokens (app_id, tenant_id, email);
```
## [10.0.3]
- Fixes `StorageTransactionLogicException` in bulk import when not using userRoles and totpDevices in import json.

View File

@ -45,7 +45,7 @@ We're happy to help!:raised_hands:
### Local Setup Prerequisites
- OS: Linux or macOS. Or if using Windows, you need to use [wsl2](https://docs.microsoft.com/en-us/windows/wsl/about).
- JDK: openjdk 15.0.1. Installation instructions for Mac and Linux can be found
- JDK: openjdk 21.0.7. Installation instructions for Mac and Linux can be found
in [our wiki](https://github.com/supertokens/supertokens-core/wiki/Installing-OpenJDK-for-Mac-and-Linux)
- IDE: [IntelliJ](https://www.jetbrains.com/idea/download/)(recommended) or equivalent IDE

View File

@ -256,7 +256,7 @@ If you think this is a project you could use in the future, please :star2: this
</tr>
<tr>
<td align="center"><a href="https://github.com/Lehoczky"><img src="https://avatars.githubusercontent.com/u/31937175?v=4" width="100px;" alt=""/><br /><sub><b>Lehoczky Zoltán</b></sub></a></td>
<td align="center"><a href="https://github.com/virajkanwade"><img src="https://avatars.githubusercontent.com/u/316111?v=4" width="100px;" alt=""/><br /><sub><b>Viraj Kanwade</b></sub></a></td>
<td align="center"><a href="https://github.com/mavwolverine"><img src="https://avatars.githubusercontent.com/u/316111?v=4" width="100px;" alt=""/><br /><sub><b>Viraj Kanwade</b></sub></a></td>
<td align="center"><a href="https://github.com/anuragmerndev"><img src="https://avatars.githubusercontent.com/u/144275260?v=4" width="100px;" alt=""/><br /><sub><b>Anurag Srivastava</b></sub></a></td>
</tr>
</table>

View File

@ -8,6 +8,8 @@
plugins {
id 'application'
id 'java-library'
id "io.freefair.aspectj" version "8.13" //same as gradle version!
}
compileJava { options.encoding = "UTF-8" }
compileTestJava { options.encoding = "UTF-8" }
@ -19,29 +21,37 @@ compileTestJava { options.encoding = "UTF-8" }
// }
//}
version = "10.0.3"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
version = "11.3.0"
repositories {
mavenCentral()
maven { url 'https://build.shibboleth.net/nexus/content/repositories/releases/' }
}
dependencies {
// https://mvnrepository.com/artifact/com.google.code.gson/gson
// if this changes, remember to also change in the ee folder's build.gradle
implementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
implementation group: 'com.google.code.gson', name: 'gson', version: '2.13.1'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1'
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.18.2'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-cbor
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-cbor', version: '2.18.2'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1'
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.2'
// https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core
implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18'
api group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '11.0.12'
// https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305
implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
@ -73,10 +83,36 @@ dependencies {
// https://mvnrepository.com/artifact/com.googlecode.libphonenumber/libphonenumber/
implementation group: 'com.googlecode.libphonenumber', name: 'libphonenumber', version: '8.13.25'
// https://mvnrepository.com/artifact/com.webauthn4j/webauthn4j-core
implementation group: 'com.webauthn4j', name: 'webauthn4j-core', version: '0.28.6.RELEASE'
implementation platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha:2.17.0-alpha")
// Open SAML
implementation group: 'org.opensaml', name: 'opensaml-core', version: '4.3.1'
implementation group: 'org.opensaml', name: 'opensaml-saml-impl', version: '4.3.1'
implementation group: 'org.opensaml', name: 'opensaml-security-impl', version: '4.3.1'
implementation group: 'org.opensaml', name: 'opensaml-profile-impl', version: '4.3.1'
implementation group: 'org.opensaml', name: 'opensaml-xmlsec-impl', version: '4.3.1'
implementation("ch.qos.logback:logback-core:1.5.18")
implementation("ch.qos.logback:logback-classic:1.5.18")
// OpenTelemetry core
implementation("io.opentelemetry:opentelemetry-sdk")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
implementation("io.opentelemetry:opentelemetry-exporter-logging")
implementation("io.opentelemetry:opentelemetry-api")
implementation("io.opentelemetry.semconv:opentelemetry-semconv")
implementation('org.aspectj:aspectjrt:1.9.24')
compileOnly project(":supertokens-plugin-interface")
testImplementation project(":supertokens-plugin-interface")
// this is so that we can find plugin-interface jar while testing
testImplementation project(":supertokens-plugin-interface")
testImplementation 'junit:junit:4.12'
// https://mvnrepository.com/artifact/org.mockito/mockito-core
@ -87,8 +123,9 @@ dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit4:0.22.0'
// https://mvnrepository.com/artifact/com.webauthn4j/webauthn4j-test
testImplementation group: 'com.webauthn4j', name: 'webauthn4j-test', version: '0.28.6.RELEASE'
}
application {
mainClass.set("io.supertokens.Main")
}
@ -98,43 +135,47 @@ jar {
}
task copyJars(type: Copy) {
into "$buildDir/dependencies"
tasks.register('copyJars', Copy) {
from configurations.runtimeClasspath
into layout.buildDirectory.dir("dependencies")
}
test {
jvmArgs '-Djava.security.egd=file:/dev/urandom'
jvmArgs = ['-Djava.security.egd=file:/dev/urandom',
"--add-opens=java.base/java.lang=ALL-UNNAMED",
"--add-opens=java.base/java.util=ALL-UNNAMED",
"--add-opens=java.base/java.util.concurrent=ALL-UNNAMED"]
testLogging {
outputs.upToDateWhen { false }
showStandardStreams = true
}
maxParallelForks = Runtime.runtime.availableProcessors()
}
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
tasks.withType(Test) {
tasks.withType(Test).configureEach {
testLogging {
// set options for log level LIFECYCLE
events TestLogEvent.FAILED,
events = [TestLogEvent.FAILED,
TestLogEvent.PASSED,
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_OUT
exceptionFormat TestExceptionFormat.FULL
showExceptions true
showCauses true
showStackTraces true
TestLogEvent.STANDARD_OUT]
exceptionFormat = TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
// set options for log level DEBUG and INFO
debug {
events TestLogEvent.STARTED,
events = [TestLogEvent.STARTED,
TestLogEvent.FAILED,
TestLogEvent.PASSED,
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_ERROR,
TestLogEvent.STANDARD_OUT
exceptionFormat TestExceptionFormat.FULL
TestLogEvent.STANDARD_OUT]
exceptionFormat = TestExceptionFormat.FULL
}
info.events = debug.events
info.exceptionFormat = debug.exceptionFormat

View File

@ -4,6 +4,8 @@ plugins {
repositories {
mavenCentral()
maven { url 'https://build.shibboleth.net/nexus/content/repositories/releases/' }
}
application {
@ -16,13 +18,13 @@ jar {
dependencies {
// https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
implementation group: 'com.google.code.gson', name: 'gson', version: '2.13.1'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1'
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.18.2'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.2'
// https://mvnrepository.com/artifact/de.mkammerer/argon2-jvm
implementation group: 'de.mkammerer', name: 'argon2-jvm', version: '2.11'
@ -33,9 +35,9 @@ dependencies {
testImplementation group: 'junit', name: 'junit', version: '4.12'
}
task copyJars(type: Copy) {
into "$buildDir/dependencies"
tasks.register('copyJars', Copy) {
from configurations.runtimeClasspath
into layout.buildDirectory.dir("dependencies")
}
test {
@ -55,10 +57,10 @@ tasks.withType(Test) {
TestLogEvent.PASSED,
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_OUT
exceptionFormat TestExceptionFormat.FULL
showExceptions true
showCauses true
showStackTraces true
exceptionFormat = TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
// set options for log level DEBUG and INFO
debug {
@ -68,7 +70,7 @@ tasks.withType(Test) {
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_ERROR,
TestLogEvent.STANDARD_OUT
exceptionFormat TestExceptionFormat.FULL
exceptionFormat = TestExceptionFormat.FULL
}
info.events = debug.events
info.exceptionFormat = debug.exceptionFormat

View File

@ -1,55 +1,40 @@
{
"_comment": "Contains list of implementation dependencies URL for this project",
"list": [
{
"jar": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1.jar",
"name": "Gson 2.3.1",
"src": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1.jar",
"name": "Jackson Dataformat 2.16.1",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar",
"name": "SnakeYAML 2.2",
"src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1.jar",
"name": "Jackson core 2.16.1",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1.jar",
"name": "Jackson databind 2.16.1",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1.jar",
"name": "Jackson annotation 2.16.1",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar",
"name": "Argon2-jvm 2.11",
"src": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm-nolibs/2.11/argon2-jvm-nolibs-2.11.jar",
"name": "Argon2-jvm no libs 2.11",
"src": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm-nolibs/2.11/argon2-jvm-nolibs-2.11-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar",
"name": "SQLite JDBC Driver 3.30.1",
"src": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0.jar",
"name": "JNA 5.8.0",
"src": "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0-sources.jar"
}
]
"_comment": "Contains list of implementation dependencies URL for this project. This is a generated file, don't modify the contents by hand.",
"list": [
{
"jar":"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.13.1/gson-2.13.1.jar",
"name":"gson 2.13.1",
"src":"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.13.1/gson-2.13.1-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.38.0/error_prone_annotations-2.38.0.jar",
"name":"error_prone_annotations 2.38.0",
"src":"https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.38.0/error_prone_annotations-2.38.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.18.2/jackson-dataformat-yaml-2.18.2.jar",
"name":"jackson-dataformat-yaml 2.18.2",
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.18.2/jackson-dataformat-yaml-2.18.2-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.3/snakeyaml-2.3.jar",
"name":"snakeyaml 2.3",
"src":"https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.3/snakeyaml-2.3-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.18.2/jackson-databind-2.18.2.jar",
"name":"jackson-databind 2.18.2",
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.18.2/jackson-databind-2.18.2-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar",
"name":"argon2-jvm 2.11",
"src":"https://repo.maven.apache.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar",
"name":"jbcrypt 0.4",
"src":"https://repo.maven.apache.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4-sources.jar"
}
]
}

Binary file not shown.

View File

@ -43,12 +43,38 @@ public class StartHandler extends CommandHandler {
String host = CLIOptionsParser.parseOption("--host", args);
boolean foreground = CLIOptionsParser.hasKey("--foreground", args);
boolean forceNoInMemDB = CLIOptionsParser.hasKey("--no-in-mem-db", args);
boolean javaagentEnabled = CLIOptionsParser.hasKey("--javaagent", args);
boolean jmxEnabled = CLIOptionsParser.hasKey("--jmx", args);
String jmxPort = CLIOptionsParser.parseOption("--jmx-port", args);
String jmxAuthenticate = CLIOptionsParser.parseOption("--jmx-authenticate", args);
String jmxSSL = CLIOptionsParser.parseOption("--jmx-ssl", args);
List<String> commands = new ArrayList<>();
if (OperatingSystem.getOS() == OperatingSystem.OS.WINDOWS) {
commands.add(installationDir + "jre\\bin\\java.exe");
commands.add("-classpath");
commands.add("\"" + installationDir + "core\\*\";\"" + installationDir + "plugin-interface\\*\"");
if (javaagentEnabled) {
commands.add("-javaagent:\"" + installationDir + "agent\\opentelemetry-javaagent.jar\"");
}
if (jmxEnabled) {
commands.add("-Dcom.sun.management.jmxremote");
if (jmxPort != null) {
commands.add("-Dcom.sun.management.jmxremote.port=" + jmxPort);
} else {
commands.add("-Dcom.sun.management.jmxremote.port=9010");
}
if (jmxAuthenticate != null) {
commands.add("-Dcom.sun.management.jmxremote.authenticate=" + jmxAuthenticate);
} else {
commands.add("-Dcom.sun.management.jmxremote.authenticate=false");
}
if (jmxSSL != null) {
commands.add("-Dcom.sun.management.jmxremote.ssl=" + jmxSSL);
} else {
commands.add("-Dcom.sun.management.jmxremote.ssl=false");
}
}
if (space != null) {
commands.add("-Xmx" + space + "M");
}
@ -77,6 +103,27 @@ public class StartHandler extends CommandHandler {
commands.add("-classpath");
commands.add(
installationDir + "core/*:" + installationDir + "plugin-interface/*:" + installationDir + "ee/*");
if (javaagentEnabled) {
commands.add("-javaagent:" + installationDir + "agent/opentelemetry-javaagent.jar");
}
if (jmxEnabled) {
commands.add("-Dcom.sun.management.jmxremote");
if (jmxPort != null) {
commands.add("-Dcom.sun.management.jmxremote.port=" + jmxPort);
} else {
commands.add("-Dcom.sun.management.jmxremote.port=9010");
}
if (jmxAuthenticate != null) {
commands.add("-Dcom.sun.management.jmxremote.authenticate=" + jmxAuthenticate);
} else {
commands.add("-Dcom.sun.management.jmxremote.authenticate=false");
}
if (jmxSSL != null) {
commands.add("-Dcom.sun.management.jmxremote.ssl=" + jmxSSL);
} else {
commands.add("-Dcom.sun.management.jmxremote.ssl=false");
}
}
if (space != null) {
commands.add("-Xmx" + space + "M");
}
@ -101,6 +148,7 @@ public class StartHandler extends CommandHandler {
if (!foreground) {
try {
ProcessBuilder pb = new ProcessBuilder(commands);
Logging.info("Command to be run: " + String.join(" ", pb.command()));
pb.redirectErrorStream(true);
Process process = pb.start();
try (InputStreamReader in = new InputStreamReader(process.getInputStream());
@ -181,6 +229,13 @@ public class StartHandler extends CommandHandler {
new Option("--foreground", "Runs this instance of SuperTokens in the foreground (not as a daemon)"));
options.add(
new Option("--with-temp-dir", "Uses the passed dir as temp dir, instead of the internal default."));
options.add(new Option("--javaagent", "Enables the OpenTelemetry Javaagent for tracing and metrics."));
options.add(new Option("--jmx", "Enables JMX management and monitoring."));
options.add(new Option("--jmx-port", "Sets the port for JMX. Defaults to 9010 if --jmx is passed."));
options.add(new Option("--jmx-authenticate",
"Sets whether JMX authentication is enabled or not. Defaults to false if --jmx is passed."));
options.add(new Option("--jmx-ssl",
"Sets whether JMX SSL is enabled or not. Defaults to false if --jmx is passed."));
return options;
}

View File

@ -174,3 +174,30 @@ core_config_version: 0
# (DIFFERENT_ACROSS_APPS | OPTIONAL | Default: number of available processor cores) int value. If specified,
# the supertokens core will use the specified number of threads to complete the migration of users.
# bulk_migration_parallelism:
# (DIFFERENT_ACROSS_APPS | OPTIONAL | Default: 8000) int value. If specified, the supertokens core will load the
# specified number of users for migrating in one single batch.
# bulk_migration_batch_size:
# (DIFFERENT_ACROSS_APPS | OPTIONAL | Default: 3600000) long value. Time in milliseconds for how long a webauthn
# account recovery token is valid for.
# webauthn_recover_account_token_lifetime:
# (OPTIONAL | Default: null) string value. The URL of the OpenTelemetry collector to which the core
# will send telemetry data. This should be in the format http://<host>:<port> or https://<host>:<port>.
# otel_collector_connection_uri:
# (OPTIONAL | Default: false) boolean value. Enables or disables the deadlock logger.
# deadlock_logger_enable:
# (OPTIONAL | Default: null) string value. If specified, uses this URL as ACS URL for handling legacy SAML clients
# saml_legacy_acs_url:
# (OPTIONAL | Default: https://saml.supertokens.com) string value. Service provider's entity ID.
# saml_sp_entity_id:
# OPTIONAL | Default: 300000) long value. Duration for which SAML claims will be valid before it is consumed
# saml_claims_validity:
# OPTIONAL | Default: 300000) long value. Duration for which SAML relay state will be valid before it is consumed
# saml_relay_state_validity:

View File

@ -21,6 +21,8 @@
"4.0",
"5.0",
"5.1",
"5.2"
"5.2",
"5.3",
"5.4"
]
}

View File

@ -175,3 +175,29 @@ disable_telemetry: true
# the supertokens core will use the specified number of threads to complete the migration of users.
# bulk_migration_parallelism:
# (DIFFERENT_ACROSS_APPS | OPTIONAL | Default: 8000) int value. If specified, the supertokens core will load the
# specified number of users for migrating in one single batch.
# bulk_migration_batch_size:
# (DIFFERENT_ACROSS_APPS | OPTIONAL | Default: 3600000) long value. Time in milliseconds for how long a webauthn
# account recovery token is valid for.
# webauthn_recover_account_token_lifetime:
# (OPTIONAL | Default: null) string value. The URL of the OpenTelemetry collector to which the core
# will send telemetry data. This should be in the format http://<host>:<port> or https://<host>:<port>.
# otel_collector_connection_uri:
# (OPTIONAL | Default: false) boolean value. Enables or disables the deadlock logger.
# deadlock_logger_enable:
# (OPTIONAL | Default: null) string value. If specified, uses this URL as ACS URL for handling legacy SAML clients
saml_legacy_acs_url: "http://localhost:5225/api/oauth/saml"
# (OPTIONAL | Default: https://saml.supertokens.com) string value. Service provider's entity ID.
# saml_sp_entity_id:
# OPTIONAL | Default: 300000) long value. Duration for which SAML claims will be valid before it is consumed
# saml_claims_validity:
# OPTIONAL | Default: 300000) long value. Duration for which SAML relay state will be valid before it is consumed
# saml_relay_state_validity:

View File

@ -18,9 +18,9 @@ dependencies {
testImplementation group: 'junit', name: 'junit', version: '4.12'
}
task copyJars(type: Copy) {
into "$buildDir/dependencies"
tasks.register('copyJars', Copy) {
from configurations.runtimeClasspath
into layout.buildDirectory.dir("dependencies")
}
test {
@ -56,10 +56,10 @@ tasks.withType(Test) {
TestLogEvent.PASSED,
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_OUT
exceptionFormat TestExceptionFormat.FULL
showExceptions true
showCauses true
showStackTraces true
exceptionFormat = TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
// set options for log level DEBUG and INFO
debug {
@ -69,7 +69,7 @@ tasks.withType(Test) {
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_ERROR,
TestLogEvent.STANDARD_OUT
exceptionFormat TestExceptionFormat.FULL
exceptionFormat = TestExceptionFormat.FULL
}
info.events = debug.events
info.exceptionFormat = debug.exceptionFormat

Binary file not shown.

View File

@ -14,6 +14,7 @@ exitIfNeeded
exitIfNeeded
(cd ../../ && ./gradlew :$prefix-core:downloader:copyJars < /dev/null)
exitIfNeeded

View File

@ -2,10 +2,12 @@ plugins {
id 'java-library'
}
version 'unspecified'
version = 'unspecified'
repositories {
mavenCentral()
maven { url 'https://build.shibboleth.net/nexus/content/repositories/releases/' }
}
jar {
@ -13,7 +15,7 @@ jar {
}
dependencies {
compileOnly group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
compileOnly group: 'com.google.code.gson', name: 'gson', version: '2.13.1'
compileOnly project(":supertokens-plugin-interface")
testImplementation project(":supertokens-plugin-interface")
@ -35,13 +37,13 @@ dependencies {
testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.1.0'
// https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core
testImplementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18'
testImplementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '11.0.5'
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14'
testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.13'
// https://mvnrepository.com/artifact/com.google.code.gson/gson
testImplementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
testImplementation group: 'com.google.code.gson', name: 'gson', version: '2.13.1'
testImplementation 'com.tngtech.archunit:archunit-junit4:0.22.0'
@ -52,17 +54,18 @@ dependencies {
testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1'
testImplementation group: 'org.jetbrains', name: 'annotations', version: '13.0'
}
task copyJars(type: Copy) {
into "$buildDir/dependencies"
tasks.register('copyJars', Copy) {
from configurations.runtimeClasspath
into layout.buildDirectory.dir("dependencies")
}
def interfaceName = "io.supertokens.featureflag.EEFeatureFlagInterface"
def className = "io.supertokens.ee.EEFeatureFlag"
task generateMetaInf {
tasks.register('generateMetaInf') {
doFirst {
mkdir "src/main/resources/META-INF/services"
file("src/main/resources/META-INF/services/${interfaceName}").text = "${className}"
@ -89,10 +92,10 @@ tasks.withType(Test) {
TestLogEvent.PASSED,
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_OUT
exceptionFormat TestExceptionFormat.FULL
showExceptions true
showCauses true
showStackTraces true
exceptionFormat = TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
// set options for log level DEBUG and INFO
debug {
@ -102,7 +105,7 @@ tasks.withType(Test) {
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_ERROR,
TestLogEvent.STANDARD_OUT
exceptionFormat TestExceptionFormat.FULL
exceptionFormat = TestExceptionFormat.FULL
}
info.events = debug.events
info.exceptionFormat = debug.exceptionFormat

Binary file not shown.

View File

@ -14,6 +14,7 @@ exitIfNeeded
exitIfNeeded
(cd ../../ && ./gradlew :$prefix-core:ee:copyJars < /dev/null)
exitIfNeeded

View File

@ -34,6 +34,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.ThirdPartyConfig;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.oauth.OAuthStorage;
import io.supertokens.pluginInterface.saml.SAMLStorage;
import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.utils.Utils;
@ -386,6 +387,34 @@ public class EEFeatureFlag implements io.supertokens.featureflag.EEFeatureFlagIn
return mauArr;
}
private JsonObject getSAMLStats() throws TenantOrAppNotFoundException, StorageQueryException {
JsonObject stats = new JsonObject();
stats.addProperty("connectionUriDomain", this.appIdentifier.getConnectionUriDomain());
stats.addProperty("appId", this.appIdentifier.getAppId());
JsonArray tenantStats = new JsonArray();
TenantConfig[] tenantConfigs = Multitenancy.getAllTenantsForApp(this.appIdentifier, main);
for (TenantConfig tenantConfig : tenantConfigs) {
JsonObject tenantStat = new JsonObject();
tenantStat.addProperty("tenantId", tenantConfig.tenantIdentifier.getTenantId());
{
Storage storage = StorageLayer.getStorage(tenantConfig.tenantIdentifier, main);
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
JsonObject stat = new JsonObject();
stat.addProperty("numberOfSAMLClients", samlStorage.countSAMLClients(tenantConfig.tenantIdentifier));
stat.add(tenantConfig.tenantIdentifier.getTenantId(), stat);
}
}
stats.add("tenants", tenantStats);
return stats;
}
@Override
public JsonObject getPaidFeatureStats() throws StorageQueryException, TenantOrAppNotFoundException {
JsonObject usageStats = new JsonObject();
@ -433,6 +462,10 @@ public class EEFeatureFlag implements io.supertokens.featureflag.EEFeatureFlagIn
if (feature == EE_FEATURES.OAUTH) {
usageStats.add(EE_FEATURES.OAUTH.toString(), getOAuthStats());
}
if (feature == EE_FEATURES.SAML) {
usageStats.add(EE_FEATURES.SAML.toString(), getSAMLStats());
}
}
usageStats.add("maus", getMAUs());
@ -523,7 +556,7 @@ public class EEFeatureFlag implements io.supertokens.featureflag.EEFeatureFlagIn
ProcessState.getInstance(main)
.addState(ProcessState.PROCESS_STATE.LICENSE_KEY_CHECK_NETWORK_CALL, null, json);
JsonObject licenseCheckResponse = HttpRequest.sendJsonPOSTRequest(this.main, REQUEST_ID,
"https://api.supertokens.io/0/st/license/check",
"https://api.supertokens.com/0/st/license/check",
json, 10000, 10000, 0);
if (licenseCheckResponse.get("status").getAsString().equalsIgnoreCase("OK")) {
Logging.debug(main, appIdentifier.getAsPublicTenantIdentifier(), "API returned OK");

View File

@ -44,14 +44,14 @@ public class TestMultitenancyStats {
String[] args = {"../../"};
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
CronTaskTest.getInstance(process.main).setIntervalInSeconds(EELicenseCheck.RESOURCE_KEY, 1);
CronTaskTest.getInstance(process.getProcess()).setIntervalInSeconds(EELicenseCheck.RESOURCE_KEY, 1);
Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}
if (StorageLayer.isInMemDb(process.main)) {
if (StorageLayer.isInMemDb(process.getProcess())) {
// cause we keep all features enabled in memdb anyway
return;
}

View File

@ -16,7 +16,7 @@ public class TestingProcessManager {
String[] args = {"../../"};
TestingProcess process = TestingProcessManager.start(args);
process.checkOrWaitForEvent(PROCESS_STATE.STARTED);
process.main.deleteAllInformationForTesting();
process.getProcess().deleteAllInformationForTesting();
process.kill();
System.out.println("----------DELETE ALL INFORMATION----------");
}

View File

@ -24,7 +24,8 @@ public abstract class Utils extends Mockito {
try {
// remove config.yaml file
ProcessBuilder pb = new ProcessBuilder("rm", "config.yaml");
String workerId = System.getProperty("org.gradle.test.worker", "");
ProcessBuilder pb = new ProcessBuilder("rm", "config" + workerId + ".yaml");
pb.directory(new File(installDir));
Process process = pb.start();
process.waitFor();
@ -58,7 +59,8 @@ public abstract class Utils extends Mockito {
// if the default config is not the same as the current config, we must reset the storage layer
File ogConfig = new File("../../temp/config.yaml");
File currentConfig = new File("../../config.yaml");
String workerId = System.getProperty("org.gradle.test.worker", "");
File currentConfig = new File("../../config" + workerId + ".yaml");
if (currentConfig.isFile()) {
byte[] ogConfigContent = Files.readAllBytes(ogConfig.toPath());
byte[] currentConfigContent = Files.readAllBytes(currentConfig.toPath());
@ -67,7 +69,7 @@ public abstract class Utils extends Mockito {
}
}
ProcessBuilder pb = new ProcessBuilder("cp", "temp/config.yaml", "./config.yaml");
ProcessBuilder pb = new ProcessBuilder("cp", "temp/config.yaml", "./config" + workerId + ".yaml");
pb.directory(new File(installDir));
Process process = pb.start();
process.waitFor();
@ -96,14 +98,15 @@ public abstract class Utils extends Mockito {
String newStr = "\n# " + key + ":";
StringBuilder originalFileContent = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader("../../config.yaml"))) {
String workerId = System.getProperty("org.gradle.test.worker", "");
try (BufferedReader reader = new BufferedReader(new FileReader("../../config" + workerId + ".yaml"))) {
String currentReadingLine = reader.readLine();
while (currentReadingLine != null) {
originalFileContent.append(currentReadingLine).append(System.lineSeparator());
currentReadingLine = reader.readLine();
}
String modifiedFileContent = originalFileContent.toString().replaceAll(oldStr, newStr);
try (BufferedWriter writer = new BufferedWriter(new FileWriter("../../config.yaml"))) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("../../config" + workerId + ".yaml"))) {
writer.write(modifiedFileContent);
}
}
@ -117,14 +120,15 @@ public abstract class Utils extends Mockito {
String oldStr = "\n((#\\s)?)" + key + "(:|((:\\s).+))\n";
String newStr = "\n" + key + ": " + value + "\n";
StringBuilder originalFileContent = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader("../../config.yaml"))) {
String workerId = System.getProperty("org.gradle.test.worker", "");
try (BufferedReader reader = new BufferedReader(new FileReader("../../config" + workerId + ".yaml"))) {
String currentReadingLine = reader.readLine();
while (currentReadingLine != null) {
originalFileContent.append(currentReadingLine).append(System.lineSeparator());
currentReadingLine = reader.readLine();
}
String modifiedFileContent = originalFileContent.toString().replaceAll(oldStr, newStr);
try (BufferedWriter writer = new BufferedWriter(new FileWriter("../../config.yaml"))) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("../../config" + workerId + ".yaml"))) {
writer.write(modifiedFileContent);
}
}

View File

@ -45,7 +45,7 @@ public class DeleteLicenseKeyAPITest {
// check that no LicenseKey exits
try {
FeatureFlag.getInstance(process.main).getLicenseKey();
FeatureFlag.getInstance(process.getProcess()).getLicenseKey();
fail();
} catch (NoLicenseKeyFoundException ignored) {
}
@ -58,7 +58,7 @@ public class DeleteLicenseKeyAPITest {
// check that no LicenseKey exits
try {
FeatureFlag.getInstance(process.main).getLicenseKey();
FeatureFlag.getInstance(process.getProcess()).getLicenseKey();
fail();
} catch (NoLicenseKeyFoundException ignored) {
}
@ -90,7 +90,7 @@ public class DeleteLicenseKeyAPITest {
// check that no LicenseKey exits
try {
FeatureFlag.getInstance(process.main).getLicenseKey();
FeatureFlag.getInstance(process.getProcess()).getLicenseKey();
fail();
} catch (NoLicenseKeyFoundException ignored) {
}

View File

@ -38,7 +38,7 @@ public class GetFeatureFlagAPITest {
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
Assert.assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STARTED));
if (StorageLayer.isInMemDb(process.main)) {
if (StorageLayer.isInMemDb(process.getProcess())) {
// cause we keep all features enabled in memdb anyway
return;
}
@ -72,7 +72,7 @@ public class GetFeatureFlagAPITest {
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
Assert.assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STARTED));
if (StorageLayer.isInMemDb(process.main)) {
if (StorageLayer.isInMemDb(process.getProcess())) {
// cause we keep all features enabled in memdb anyway
return;
}

View File

@ -85,7 +85,7 @@ public class GetLicenseKeyAPITest {
assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STARTED));
Assert.assertNull(FeatureFlag.getInstance(process.main).getEeFeatureFlagInstance());
Assert.assertNull(FeatureFlag.getInstance(process.getProcess()).getEeFeatureFlagInstance());
Assert.assertEquals(FeatureFlag.getInstance(process.getProcess()).getEnabledFeatures().length, 0);

View File

@ -74,9 +74,9 @@ public class SetLicenseKeyAPITest {
assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STARTED));
Assert.assertNull(FeatureFlag.getInstance(process.main).getEeFeatureFlagInstance());
Assert.assertNull(FeatureFlag.getInstance(process.getProcess()).getEeFeatureFlagInstance());
Assert.assertEquals(FeatureFlag.getInstance(process.getProcess()).getEnabledFeatures().length, 0);
Assert.assertEquals(0, FeatureFlag.getInstance(process.getProcess()).getEnabledFeatures().length);
// set license key when ee folder does not exist
JsonObject requestBody = new JsonObject();

View File

@ -1,120 +1,285 @@
{
"_comment": "Contains list of implementation dependencies URL for this project",
"list": [
{
"jar": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1.jar",
"name": "Gson 2.3.1",
"src": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1.jar",
"name": "Jackson Dataformat 2.16.1",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar",
"name": "SnakeYAML 2.2",
"src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1.jar",
"name": "Jackson core 2.16.1",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1.jar",
"name": "Jackson databind 2.16.1",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1.jar",
"name": "Jackson annotation 2.16.1",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.4.14/logback-classic-1.4.14.jar",
"name": "Logback classic 1.4.14",
"src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.4.14/logback-classic-1.4.14-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.4.14/logback-core-1.4.14.jar",
"name": "Logback core 1.4.14",
"src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.4.14/logback-core-1.4.14-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.7/slf4j-api-2.0.7.jar",
"name": "SLF4j API 2.0.7",
"src": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.7/slf4j-api-2.0.7-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.18/tomcat-annotations-api-10.1.18.jar",
"name": "Tomcat annotations API 10.1.18",
"src": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.18/tomcat-annotations-api-10.1.18-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18.jar",
"name": "Tomcat embed core API 10.1.1",
"src": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar",
"name": "JSR305 3.0.2",
"src": "https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.jar",
"name": "JSR305 3.0.2",
"src": "https://repo1.maven.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar",
"name": "SQLite JDBC Driver 3.45.1.0",
"src": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar",
"name": "JBCrypt 0.4",
"src": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/auth0/java-jwt/4.4.0/java-jwt-4.4.0.jar",
"name": "Auth0 Java JWT",
"src": "https://repo1.maven.org/maven2/com/auth0/java-jwt/4.4.0/java-jwt-4.4.0-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar",
"name": "Argon2-jvm 2.11",
"src": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm-nolibs/2.11/argon2-jvm-nolibs-2.11.jar",
"name": "Argon2-jvm no libs 2.11",
"src": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm-nolibs/2.11/argon2-jvm-nolibs-2.11-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0.jar",
"name": "JNA 5.8.0",
"src": "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0.jar",
"name": "Scrypt 1.4.0",
"src": "https://repo1.maven.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/eatthepath/java-otp/0.4.0/java-otp-0.4.0.jar",
"name": "Java OTP 0.4.0",
"src": "https://repo1.maven.org/maven2/com/eatthepath/java-otp/0.4.0/java-otp-0.4.0-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15.jar",
"name": "Commons Codec 1.15",
"src": "https://repo1.maven.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.25/libphonenumber-8.13.25.jar",
"name": "Libphonenumber 8.13.25",
"src": "https://repo1.maven.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.25/libphonenumber-8.13.25-sources.jar"
}
]
"_comment": "Contains list of implementation dependencies URL for this project. This is a generated file, don't modify the contents by hand.",
"list": [
{
"jar":"https://repo.maven.apache.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/11.0.12/tomcat-embed-core-11.0.12.jar",
"name":"tomcat-embed-core 11.0.12",
"src":"https://repo.maven.apache.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/11.0.12/tomcat-embed-core-11.0.12-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat-annotations-api/11.0.12/tomcat-annotations-api-11.0.12.jar",
"name":"tomcat-annotations-api 11.0.12",
"src":"https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat-annotations-api/11.0.12/tomcat-annotations-api-11.0.12-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.13.1/gson-2.13.1.jar",
"name":"gson 2.13.1",
"src":"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.13.1/gson-2.13.1-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.38.0/error_prone_annotations-2.38.0.jar",
"name":"error_prone_annotations 2.38.0",
"src":"https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.38.0/error_prone_annotations-2.38.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.18.2/jackson-dataformat-yaml-2.18.2.jar",
"name":"jackson-dataformat-yaml 2.18.2",
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.18.2/jackson-dataformat-yaml-2.18.2-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.3/snakeyaml-2.3.jar",
"name":"snakeyaml 2.3",
"src":"https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.3/snakeyaml-2.3-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.18.2/jackson-dataformat-cbor-2.18.2.jar",
"name":"jackson-dataformat-cbor 2.18.2",
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.18.2/jackson-dataformat-cbor-2.18.2-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.18.2/jackson-databind-2.18.2.jar",
"name":"jackson-databind 2.18.2",
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.18.2/jackson-databind-2.18.2-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar",
"name":"jsr305 3.0.2",
"src":"https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar",
"name":"sqlite-jdbc 3.45.1.0",
"src":"https://repo.maven.apache.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar",
"name":"slf4j-api 2.0.17",
"src":"https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar",
"name":"jbcrypt 0.4",
"src":"https://repo.maven.apache.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.jar",
"name":"annotations 13.0",
"src":"https://repo.maven.apache.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar",
"name":"argon2-jvm 2.11",
"src":"https://repo.maven.apache.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/auth0/java-jwt/4.4.0/java-jwt-4.4.0.jar",
"name":"java-jwt 4.4.0",
"src":"https://repo.maven.apache.org/maven2/com/auth0/java-jwt/4.4.0/java-jwt-4.4.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0.jar",
"name":"scrypt 1.4.0",
"src":"https://repo.maven.apache.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/eatthepath/java-otp/0.4.0/java-otp-0.4.0.jar",
"name":"java-otp 0.4.0",
"src":"https://repo.maven.apache.org/maven2/com/eatthepath/java-otp/0.4.0/java-otp-0.4.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15.jar",
"name":"commons-codec 1.15",
"src":"https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.25/libphonenumber-8.13.25.jar",
"name":"libphonenumber 8.13.25",
"src":"https://repo.maven.apache.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.25/libphonenumber-8.13.25-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/webauthn4j/webauthn4j-core/0.28.6.RELEASE/webauthn4j-core-0.28.6.RELEASE.jar",
"name":"webauthn4j-core 0.28.6.RELEASE",
"src":"https://repo.maven.apache.org/maven2/com/webauthn4j/webauthn4j-core/0.28.6.RELEASE/webauthn4j-core-0.28.6.RELEASE-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-core/4.3.1/opensaml-core-4.3.1.jar",
"name":"opensaml-core 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-core/4.3.1/opensaml-core-4.3.1-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/net/shibboleth/utilities/java-support/8.4.1/java-support-8.4.1.jar",
"name":"java-support 8.4.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/net/shibboleth/utilities/java-support/8.4.1/java-support-8.4.1-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/google/guava/guava/31.1-jre/guava-31.1-jre.jar",
"name":"guava 31.1-jre",
"src":"https://repo.maven.apache.org/maven2/com/google/guava/guava/31.1-jre/guava-31.1-jre-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar",
"name":"failureaccess 1.0.1",
"src":"https://repo.maven.apache.org/maven2/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar",
"name":"listenablefuture 9999.0-empty-to-avoid-conflict-with-guava",
"src":"https://repo.maven.apache.org/maven2/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar",
"name":"j2objc-annotations 1.3",
"src":"https://repo.maven.apache.org/maven2/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/io/dropwizard/metrics/metrics-core/4.2.25/metrics-core-4.2.25.jar",
"name":"metrics-core 4.2.25",
"src":"https://repo.maven.apache.org/maven2/io/dropwizard/metrics/metrics-core/4.2.25/metrics-core-4.2.25-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-saml-impl/4.3.1/opensaml-saml-impl-4.3.1.jar",
"name":"opensaml-saml-impl 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-saml-impl/4.3.1/opensaml-saml-impl-4.3.1-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-xmlsec-impl/4.3.1/opensaml-xmlsec-impl-4.3.1.jar",
"name":"opensaml-xmlsec-impl 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-xmlsec-impl/4.3.1/opensaml-xmlsec-impl-4.3.1-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-security-impl/4.3.1/opensaml-security-impl-4.3.1.jar",
"name":"opensaml-security-impl 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-security-impl/4.3.1/opensaml-security-impl-4.3.1-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-security-api/4.3.1/opensaml-security-api-4.3.1.jar",
"name":"opensaml-security-api 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-security-api/4.3.1/opensaml-security-api-4.3.1-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-messaging-api/4.3.1/opensaml-messaging-api-4.3.1.jar",
"name":"opensaml-messaging-api 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-messaging-api/4.3.1/opensaml-messaging-api-4.3.1-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.jar",
"name":"httpclient 4.5.14",
"src":"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16.jar",
"name":"httpcore 4.4.16",
"src":"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/cryptacular/cryptacular/1.2.5/cryptacular-1.2.5.jar",
"name":"cryptacular 1.2.5",
"src":"https://repo.maven.apache.org/maven2/org/cryptacular/cryptacular/1.2.5/cryptacular-1.2.5-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk18on/1.72/bcprov-jdk18on-1.72.jar",
"name":"bcprov-jdk18on 1.72",
"src":"https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk18on/1.72/bcprov-jdk18on-1.72-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/bouncycastle/bcpkix-jdk18on/1.72/bcpkix-jdk18on-1.72.jar",
"name":"bcpkix-jdk18on 1.72",
"src":"https://repo.maven.apache.org/maven2/org/bouncycastle/bcpkix-jdk18on/1.72/bcpkix-jdk18on-1.72-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/bouncycastle/bcutil-jdk18on/1.72/bcutil-jdk18on-1.72.jar",
"name":"bcutil-jdk18on 1.72",
"src":"https://repo.maven.apache.org/maven2/org/bouncycastle/bcutil-jdk18on/1.72/bcutil-jdk18on-1.72-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-xmlsec-api/4.3.1/opensaml-xmlsec-api-4.3.1.jar",
"name":"opensaml-xmlsec-api 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-xmlsec-api/4.3.1/opensaml-xmlsec-api-4.3.1-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/apache/santuario/xmlsec/2.3.4/xmlsec-2.3.4.jar",
"name":"xmlsec 2.3.4",
"src":"https://repo.maven.apache.org/maven2/org/apache/santuario/xmlsec/2.3.4/xmlsec-2.3.4-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-saml-api/4.3.1/opensaml-saml-api-4.3.1.jar",
"name":"opensaml-saml-api 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-saml-api/4.3.1/opensaml-saml-api-4.3.1-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-profile-api/4.3.1/opensaml-profile-api-4.3.1.jar",
"name":"opensaml-profile-api 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-profile-api/4.3.1/opensaml-profile-api-4.3.1-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-soap-api/4.3.1/opensaml-soap-api-4.3.1.jar",
"name":"opensaml-soap-api 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-soap-api/4.3.1/opensaml-soap-api-4.3.1-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-soap-impl/4.3.1/opensaml-soap-impl-4.3.1.jar",
"name":"opensaml-soap-impl 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-soap-impl/4.3.1/opensaml-soap-impl-4.3.1-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-storage-api/4.3.1/opensaml-storage-api-4.3.1.jar",
"name":"opensaml-storage-api 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-storage-api/4.3.1/opensaml-storage-api-4.3.1-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/apache/velocity/velocity-engine-core/2.3/velocity-engine-core-2.3.jar",
"name":"velocity-engine-core 2.3",
"src":"https://repo.maven.apache.org/maven2/org/apache/velocity/velocity-engine-core/2.3/velocity-engine-core-2.3-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.11/commons-lang3-3.11.jar",
"name":"commons-lang3 3.11",
"src":"https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.11/commons-lang3-3.11-sources.jar"
},
{
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-profile-impl/4.3.1/opensaml-profile-impl-4.3.1.jar",
"name":"opensaml-profile-impl 4.3.1",
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-profile-impl/4.3.1/opensaml-profile-impl-4.3.1-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/ch/qos/logback/logback-core/1.5.18/logback-core-1.5.18.jar",
"name":"logback-core 1.5.18",
"src":"https://repo.maven.apache.org/maven2/ch/qos/logback/logback-core/1.5.18/logback-core-1.5.18-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/ch/qos/logback/logback-classic/1.5.18/logback-classic-1.5.18.jar",
"name":"logback-classic 1.5.18",
"src":"https://repo.maven.apache.org/maven2/ch/qos/logback/logback-classic/1.5.18/logback-classic-1.5.18-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/org/aspectj/aspectjrt/1.9.24/aspectjrt-1.9.24.jar",
"name":"aspectjrt 1.9.24",
"src":"https://repo.maven.apache.org/maven2/org/aspectj/aspectjrt/1.9.24/aspectjrt-1.9.24-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-api/1.51.0/opentelemetry-api-1.51.0.jar",
"name":"opentelemetry-api 1.51.0",
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-api/1.51.0/opentelemetry-api-1.51.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-exporter-logging/1.51.0/opentelemetry-exporter-logging-1.51.0.jar",
"name":"opentelemetry-exporter-logging 1.51.0",
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-exporter-logging/1.51.0/opentelemetry-exporter-logging-1.51.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-sdk/1.51.0/opentelemetry-sdk-1.51.0.jar",
"name":"opentelemetry-sdk 1.51.0",
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-sdk/1.51.0/opentelemetry-sdk-1.51.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-exporter-otlp/1.51.0/opentelemetry-exporter-otlp-1.51.0.jar",
"name":"opentelemetry-exporter-otlp 1.51.0",
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-exporter-otlp/1.51.0/opentelemetry-exporter-otlp-1.51.0-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/semconv/opentelemetry-semconv/1.34.0/opentelemetry-semconv-1.34.0.jar",
"name":"opentelemetry-semconv 1.34.0",
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/semconv/opentelemetry-semconv/1.34.0/opentelemetry-semconv-1.34.0-sources.jar"
}
]
}

Binary file not shown.

BIN
jar/core-11.3.0.jar Normal file

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"_comment": "contains a list of plugin interfaces branch names that this core supports",
"versions": [
"7.0"
"8.3"
]
}

View File

@ -24,7 +24,7 @@ public class ActiveUsers {
@TestOnly
public static void updateLastActive(Main main, String userId) {
try {
ActiveUsers.updateLastActive(new AppIdentifier(null, null),
ActiveUsers.updateLastActive(ResourceDistributor.getAppForTesting().toAppIdentifier(),
main, userId);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -55,6 +55,6 @@ public class ActiveUsers {
@TestOnly
public static int countUsersActiveSince(Main main, long time)
throws StorageQueryException, TenantOrAppNotFoundException {
return countUsersActiveSince(main, new AppIdentifier(null, null), time);
return countUsersActiveSince(main, ResourceDistributor.getAppForTesting().toAppIdentifier(), time);
}
}

View File

@ -22,6 +22,9 @@ import io.supertokens.config.CoreConfig;
import io.supertokens.cronjobs.Cronjobs;
import io.supertokens.cronjobs.bulkimport.ProcessBulkImportUsers;
import io.supertokens.cronjobs.cleanupOAuthSessionsAndChallenges.CleanupOAuthSessionsAndChallenges;
import io.supertokens.cronjobs.deleteExpiredSAMLData.DeleteExpiredSAMLData;
import io.supertokens.cronjobs.cleanupWebauthnExpiredData.CleanUpWebauthNExpiredDataCron;
import io.supertokens.cronjobs.deadlocklogger.DeadlockLogger;
import io.supertokens.cronjobs.deleteExpiredAccessTokenSigningKeys.DeleteExpiredAccessTokenSigningKeys;
import io.supertokens.cronjobs.deleteExpiredDashboardSessions.DeleteExpiredDashboardSessions;
import io.supertokens.cronjobs.deleteExpiredEmailVerificationTokens.DeleteExpiredEmailVerificationTokens;
@ -41,7 +44,9 @@ import io.supertokens.pluginInterface.exceptions.DbInitException;
import io.supertokens.pluginInterface.exceptions.InvalidConfigException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.saml.SAMLBootstrap;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.telemetry.TelemetryProvider;
import io.supertokens.version.Version;
import io.supertokens.webserver.Webserver;
import org.jetbrains.annotations.TestOnly;
@ -90,6 +95,9 @@ public class Main {
private boolean waitToEnableFeatureFlag = false;
private final Object waitToEnableFeatureFlagLock = new Object();
//setting to true by default
private final Boolean bulkMigrationCronEnabled = System.getenv("BULK_MIGRATION_CRON_ENABLED") == null || Boolean.parseBoolean(System.getenv("BULK_MIGRATION_CRON_ENABLED"));
private boolean forceInMemoryDB = false;
@ -116,6 +124,8 @@ public class Main {
CLIOptions.load(this, args);
init();
} catch (Exception e) {
Logging.error(this, TenantIdentifier.BASE_TENANT, "What caused the crash: " + e.getMessage(), true,
e);
ProcessState.getInstance(this).addState(ProcessState.PROCESS_STATE.INIT_FAILURE, e);
throw e;
}
@ -150,9 +160,12 @@ public class Main {
// Handle kill signal gracefully
handleKillSignalForWhenItHappens();
StorageLayer.loadStorageUCL(CLIOptions.get(this).getInstallationPath() + "plugin/");
// loading configs for core from config.yaml file.
try {
Config.loadBaseConfig(this);
Logging.info(this, TenantIdentifier.BASE_TENANT, "Completed config.yaml loading.", true);
} catch (InvalidConfigException e) {
throw new QuitProgramException(e);
}
@ -160,12 +173,11 @@ public class Main {
// loading version file
Version.loadVersion(this, CLIOptions.get(this).getInstallationPath() + "version.yaml");
Logging.info(this, TenantIdentifier.BASE_TENANT, "Completed config.yaml loading.", true);
TelemetryProvider.initialize(this);
// loading storage layer
try {
StorageLayer.initPrimary(this, CLIOptions.get(this).getInstallationPath() + "plugin/",
Config.getBaseConfigAsJsonObject(this));
StorageLayer.initPrimary(this, Config.getBaseConfigAsJsonObject(this));
} catch (InvalidConfigException e) {
throw new QuitProgramException(e);
}
@ -173,6 +185,9 @@ public class Main {
// init file logging
Logging.initFileLogging(this);
// Required for SAML related stuff
SAMLBootstrap.initialize();
// initialise cron job handler
Cronjobs.init(this);
@ -261,10 +276,21 @@ public class Main {
Cronjobs.addCronjob(this, DeleteExpiredAccessTokenSigningKeys.init(this, uniqueUserPoolIdsTenants));
// initializes ProcessBulkImportUsers cronjob to process bulk import users
Cronjobs.addCronjob(this, ProcessBulkImportUsers.init(this, uniqueUserPoolIdsTenants));
if(bulkMigrationCronEnabled) {
Cronjobs.addCronjob(this, ProcessBulkImportUsers.init(this, uniqueUserPoolIdsTenants));
}
Cronjobs.addCronjob(this, CleanupOAuthSessionsAndChallenges.init(this, uniqueUserPoolIdsTenants));
Cronjobs.addCronjob(this, CleanUpWebauthNExpiredDataCron.init(this, uniqueUserPoolIdsTenants));
// starts the DeadlockLogger if
if (Config.getBaseConfig(this).isDeadlockLoggerEnabled()) {
DeadlockLogger.getInstance().start();
}
Cronjobs.addCronjob(this, DeleteExpiredSAMLData.init(this, uniqueUserPoolIdsTenants));
// this is to ensure tenantInfos are in sync for the new cron job as well
MultitenancyHelper.getInstance(this).refreshCronjobs();
@ -354,12 +380,16 @@ public class Main {
}
private void createDotStartedFileForThisProcess() throws IOException {
String startedDir = ".started";
if (isTesting) {
startedDir = ".started" + System.getProperty("org.gradle.test.worker", "");
}
CoreConfig config = Config.getBaseConfig(this);
String fileLocation = CLIOptions.get(this).getTempDirLocation() == null ? CLIOptions.get(this).getInstallationPath() : CLIOptions.get(this).getTempDirLocation();
String fileName = OperatingSystem.getOS() == OperatingSystem.OS.WINDOWS
? fileLocation + ".started\\" + config.getHost(this) + "-"
? fileLocation + startedDir + "\\" + config.getHost(this) + "-"
+ config.getPort(this)
: fileLocation + ".started/" + config.getHost(this) + "-"
: fileLocation + startedDir + "/" + config.getHost(this) + "-"
+ config.getPort(this);
File dotStarted = new File(fileName);
if (!dotStarted.exists()) {
@ -402,9 +432,10 @@ public class Main {
@TestOnly
public void killForTestingAndWaitForShutdown() throws InterruptedException {
assertIsTesting();
wakeUpMainThreadToShutdown();
mainThread.join();
// Do not kill for now
assertIsTesting();
wakeUpMainThreadToShutdown();
mainThread.join();
}
// must not throw any error
@ -433,6 +464,7 @@ public class Main {
StorageLayer.close(this);
removeDotStartedFileForThisProcess();
Logging.stopLogging(this);
TelemetryProvider.closeTelemetry(this);
// uncomment this when you want to confirm that processes are actually shut.
// printRunningThreadNames();

View File

@ -104,7 +104,7 @@ public class ProcessState extends ResourceDistributor.SingletonResource {
public static class EventAndException {
public Exception exception;
public JsonObject data;
PROCESS_STATE state;
public PROCESS_STATE state;
public EventAndException(PROCESS_STATE state, Exception e) {
this.state = state;

View File

@ -35,16 +35,28 @@ public class ResourceDistributor {
private final Map<KeyClass, SingletonResource> resources = new HashMap<>(1);
private final Main main;
private static TenantIdentifier appUsedForTesting = TenantIdentifier.BASE_TENANT;
public ResourceDistributor(Main main) {
this.main = main;
}
public synchronized SingletonResource getResource(AppIdentifier appIdentifier, @Nonnull String key)
@TestOnly
public static void setAppForTesting(TenantIdentifier app) {
appUsedForTesting = app;
}
@TestOnly
public static TenantIdentifier getAppForTesting() {
return appUsedForTesting;
}
public SingletonResource getResource(AppIdentifier appIdentifier, @Nonnull String key)
throws TenantOrAppNotFoundException {
return getResource(appIdentifier.getAsPublicTenantIdentifier(), key);
}
public synchronized SingletonResource getResource(TenantIdentifier tenantIdentifier, @Nonnull String key)
public SingletonResource getResource(TenantIdentifier tenantIdentifier, @Nonnull String key)
throws TenantOrAppNotFoundException {
// first we do exact match
SingletonResource resource = resources.get(new KeyClass(tenantIdentifier, key));
@ -58,14 +70,6 @@ public class ResourceDistributor {
throw new TenantOrAppNotFoundException(tenantIdentifier);
}
MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true);
// we try again..
resource = resources.get(new KeyClass(tenantIdentifier, key));
if (resource != null) {
return resource;
}
// then we see if the user has configured anything to do with connectionUriDomain, and if they have,
// then we must return null cause the user has not specifically added tenantId to it
for (KeyClass currKey : resources.keySet()) {
@ -89,11 +93,11 @@ public class ResourceDistributor {
}
@TestOnly
public synchronized SingletonResource getResource(@Nonnull String key) {
return resources.get(new KeyClass(new TenantIdentifier(null, null, null), key));
public SingletonResource getResource(@Nonnull String key) {
return resources.get(new KeyClass(appUsedForTesting, key));
}
public synchronized SingletonResource setResource(TenantIdentifier tenantIdentifier,
public SingletonResource setResource(TenantIdentifier tenantIdentifier,
@Nonnull String key,
SingletonResource resource) {
SingletonResource alreadyExists = resources.get(new KeyClass(tenantIdentifier, key));
@ -104,7 +108,7 @@ public class ResourceDistributor {
return resource;
}
public synchronized SingletonResource removeResource(TenantIdentifier tenantIdentifier,
public SingletonResource removeResource(TenantIdentifier tenantIdentifier,
@Nonnull String key) {
SingletonResource singletonResource = resources.get(new KeyClass(tenantIdentifier, key));
if (singletonResource == null) {
@ -114,18 +118,18 @@ public class ResourceDistributor {
return singletonResource;
}
public synchronized SingletonResource setResource(AppIdentifier appIdentifier,
public SingletonResource setResource(AppIdentifier appIdentifier,
@Nonnull String key,
SingletonResource resource) {
return setResource(appIdentifier.getAsPublicTenantIdentifier(), key, resource);
}
public synchronized SingletonResource removeResource(AppIdentifier appIdentifier,
public SingletonResource removeResource(AppIdentifier appIdentifier,
@Nonnull String key) {
return removeResource(appIdentifier.getAsPublicTenantIdentifier(), key);
}
public synchronized void clearAllResourcesWithResourceKey(String inputKey) {
public void clearAllResourcesWithResourceKey(String inputKey) {
List<KeyClass> toRemove = new ArrayList<>();
resources.forEach((key, value) -> {
if (key.key.equals(inputKey)) {
@ -137,7 +141,7 @@ public class ResourceDistributor {
}
}
public synchronized Map<KeyClass, SingletonResource> getAllResourcesWithResourceKey(String inputKey) {
public Map<KeyClass, SingletonResource> getAllResourcesWithResourceKey(String inputKey) {
Map<KeyClass, SingletonResource> result = new HashMap<>();
resources.forEach((key, value) -> {
if (key.key.equals(inputKey)) {
@ -148,9 +152,9 @@ public class ResourceDistributor {
}
@TestOnly
public synchronized SingletonResource setResource(@Nonnull String key,
public SingletonResource setResource(@Nonnull String key,
SingletonResource resource) {
return setResource(new TenantIdentifier(null, null, null), key, resource);
return setResource(appUsedForTesting, key, resource);
}
public interface Func<T> {

View File

@ -17,10 +17,9 @@
package io.supertokens.authRecipe;
import io.supertokens.Main;
import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException;
import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException;
import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException;
import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithPrimaryUserIdException;
import io.supertokens.ResourceDistributor;
import io.supertokens.authRecipe.exception.*;
import io.supertokens.bulkimport.BulkImportUserUtils;
import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.pluginInterface.RECIPE_ID;
@ -29,6 +28,7 @@ import io.supertokens.pluginInterface.StorageUtils;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.authRecipe.LoginMethod;
import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage;
import io.supertokens.pluginInterface.bulkimport.BulkImportUser;
import io.supertokens.pluginInterface.bulkimport.exceptions.BulkImportBatchInsertException;
import io.supertokens.pluginInterface.dashboard.DashboardSearchTags;
import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
@ -44,6 +44,7 @@ import io.supertokens.session.Session;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.useridmapping.UserIdType;
import io.supertokens.utils.Utils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import javax.annotation.Nullable;
@ -59,7 +60,7 @@ public class AuthRecipe {
@TestOnly
public static boolean unlinkAccounts(Main main, String recipeUserId)
throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException {
return unlinkAccounts(main, new AppIdentifier(null, null), StorageLayer.getStorage(main), recipeUserId);
return unlinkAccounts(main, ResourceDistributor.getAppForTesting().toAppIdentifier(), StorageLayer.getStorage(main), recipeUserId);
}
@ -124,7 +125,7 @@ public class AuthRecipe {
@TestOnly
public static AuthRecipeUserInfo getUserById(Main main, String userId)
throws StorageQueryException {
return getUserById(new AppIdentifier(null, null), StorageLayer.getStorage(main), userId);
return getUserById(ResourceDistributor.getAppForTesting().toAppIdentifier(), StorageLayer.getStorage(main), userId);
}
public static AuthRecipeUserInfo getUserById(AppIdentifier appIdentifier, Storage storage, String userId)
@ -155,12 +156,15 @@ public class AuthRecipe {
}
public static class CreatePrimaryUserBulkResult {
public AuthRecipeUserInfo user;
public BulkImportUser user;
public BulkImportUser.LoginMethod primaryLoginMethod;
public boolean wasAlreadyAPrimaryUser;
public Exception error;
public CreatePrimaryUserBulkResult(AuthRecipeUserInfo user, boolean wasAlreadyAPrimaryUser, Exception error) {
public CreatePrimaryUserBulkResult(BulkImportUser user, BulkImportUser.LoginMethod primaryLoginMethod,
boolean wasAlreadyAPrimaryUser, Exception error) {
this.user = user;
this.primaryLoginMethod = primaryLoginMethod;
this.wasAlreadyAPrimaryUser = wasAlreadyAPrimaryUser;
this.error = error;
}
@ -182,16 +186,16 @@ public class AuthRecipe {
public String recipeUserId;
public String primaryUserId;
public Exception error;
public AuthRecipeUserInfo authRecipeUserInfo;
public BulkImportUser bulkImportUser;
public boolean alreadyLinked;
public CanLinkAccountsBulkResult(String recipeUserId, String primaryUserId, boolean alreadyLinked, Exception error,
AuthRecipeUserInfo authRecipeUserInfo) {
BulkImportUser bulkImportUser) {
this.recipeUserId = recipeUserId;
this.primaryUserId = primaryUserId;
this.alreadyLinked = alreadyLinked;
this.error = error;
this.authRecipeUserInfo = authRecipeUserInfo;
this.bulkImportUser = bulkImportUser;
}
}
@ -200,7 +204,7 @@ public class AuthRecipe {
throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException,
RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException,
AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException {
return canLinkAccounts(new AppIdentifier(null, null), StorageLayer.getStorage(main), recipeUserId,
return canLinkAccounts(ResourceDistributor.getAppForTesting().toAppIdentifier(), StorageLayer.getStorage(main), recipeUserId,
primaryUserId);
}
@ -291,67 +295,55 @@ public class AuthRecipe {
return new CanLinkAccountsResult(recipeUser.getSupertokensUserId(), primaryUser.getSupertokensUserId(), false);
}
private static List<CanLinkAccountsBulkResult> canLinkMultipleAccountsHelper(TransactionConnection con,
AppIdentifier appIdentifier,
Storage storage,
Map<String, String> recipeUserIdByPrimaryUserId,
List<String> allDistinctEmailAddresses,
List<String> phones,
Map<String, String> thirdpartyUserIdToId)
private static List<CanLinkAccountsBulkResult> canLinkMultipleAccountsHelperForBulkImport(TransactionConnection con,
AppIdentifier appIdentifier,
Storage storage,
List<BulkImportUser> users,
List<AuthRecipeUserInfo> allUsersWithExtraData)
throws StorageQueryException {
AuthRecipeSQLStorage authRecipeStorage = StorageUtils.getAuthRecipeStorage(storage);
List<CanLinkAccountsBulkResult> results = new ArrayList<>();
List<AuthRecipeUserInfo> primaryUsers = authRecipeStorage.getPrimaryUsersByIds_Transaction(appIdentifier, con,
new ArrayList<>(recipeUserIdByPrimaryUserId.values()));
Map<String, String> recipeUserIdByPrimaryUserId = BulkImportUserUtils.collectRecipeIdsToPrimaryIds(users);
List<AuthRecipeUserInfo> recipeUsers = authRecipeStorage.getPrimaryUsersByIds_Transaction(appIdentifier, con,
new ArrayList<>(recipeUserIdByPrimaryUserId.keySet()));
List<AuthRecipeUserInfo> allUsersWithExtraData =
List.of(authRecipeStorage.listPrimaryUsersByMultipleEmailsOrPhoneNumbersOrThirdparty_Transaction
(appIdentifier, con, allDistinctEmailAddresses, phones, thirdpartyUserIdToId));
if(recipeUsers != null && primaryUsers != null) {
//collect all the really primary users into a map of userid -> authRecipeUserInfo
Map<String, AuthRecipeUserInfo> foundValidPrimaryUsers = primaryUsers.stream().filter(authRecipeUserInfo -> authRecipeUserInfo.isPrimaryUser).collect(Collectors.toMap(AuthRecipeUserInfo::getSupertokensUserId, authRecipeUserInfo -> authRecipeUserInfo));
Map<String, AuthRecipeUserInfo> foundRecipeUsers = recipeUsers.stream().collect(Collectors.toMap(AuthRecipeUserInfo::getSupertokensUserId, authRecipeUserInfo -> authRecipeUserInfo));
if(recipeUserIdByPrimaryUserId != null && !recipeUserIdByPrimaryUserId.isEmpty()) {
for(Map.Entry<String, String> recipeUserByPrimaryUser : recipeUserIdByPrimaryUserId.entrySet()) {
String recipeUserId = recipeUserByPrimaryUser.getKey();
String primaryUserId = recipeUserByPrimaryUser.getValue();
AuthRecipeUserInfo primaryUser = foundValidPrimaryUsers.get(primaryUserId);
AuthRecipeUserInfo recipeUser = foundRecipeUsers.get(recipeUserId);
BulkImportUser.LoginMethod primaryUser = BulkImportUserUtils.findLoginMethodByRecipeUserId(users, primaryUserId);
BulkImportUser.LoginMethod recipeUser = BulkImportUserUtils.findLoginMethodByRecipeUserId(users, recipeUserId);
if(primaryUser == null || recipeUser == null) {
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false, new UnknownUserIdException(), null));
} else if(recipeUser.isPrimaryUser) {
if (recipeUser.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) {
} else if(recipeUser.isPrimary) {
if (recipeUser.superTokensUserId.equals(primaryUser.superTokensUserId)) {
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, true, null, null));
} else {
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false, new RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(recipeUser, "The input recipe user ID is already linked to another user ID"), null));
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false,
new BulkImportRecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(recipeUserId), null));
}
} else {
if (recipeUser.loginMethods.length == 1) {
Set<String> tenantIds = new HashSet<>();
tenantIds.addAll(recipeUser.tenantIds);
tenantIds.addAll(primaryUser.tenantIds);
try {
Set<String> tenantIds = new HashSet<>();
tenantIds.addAll(recipeUser.tenantIds);
tenantIds.addAll(primaryUser.tenantIds);
try {
bulkCheckIfLoginMethodCanBeLinkedOnTenant(con, appIdentifier, authRecipeStorage, tenantIds,
recipeUser, primaryUserId, allUsersWithExtraData);
BulkImportUser currentPrimaryUser = BulkImportUserUtils.findUserByPrimaryId(users, primaryUserId);
for (BulkImportUser.LoginMethod currLoginMethod : currentPrimaryUser.loginMethods) {
bulkCheckIfLoginMethodCanBeLinkedOnTenant(con, appIdentifier, authRecipeStorage, tenantIds,
recipeUser.loginMethods[0], primaryUser, allUsersWithExtraData);
for (LoginMethod currLoginMethod : primaryUser.loginMethods) {
bulkCheckIfLoginMethodCanBeLinkedOnTenant(con, appIdentifier, authRecipeStorage, tenantIds,
currLoginMethod, primaryUser, allUsersWithExtraData);
}
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false, null, primaryUser));
} catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException exception) {
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false, exception, null));
currLoginMethod, primaryUserId, allUsersWithExtraData);
}
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false, null, currentPrimaryUser));
} catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException exception) {
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false, exception, null));
}
}
}
}
@ -433,8 +425,8 @@ public class AuthRecipe {
private static void bulkCheckIfLoginMethodCanBeLinkedOnTenant(TransactionConnection con, AppIdentifier appIdentifier,
AuthRecipeSQLStorage authRecipeStorage,
Set<String> tenantIds, LoginMethod currLoginMethod,
AuthRecipeUserInfo primaryUser,
Set<String> tenantIds, BulkImportUser.LoginMethod currLoginMethod,
String primaryUserId,
List<AuthRecipeUserInfo> allUsersWithExtraData)
throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException {
// we loop through the union of both the user's tenantIds and check that the criteria for
@ -461,7 +453,7 @@ public class AuthRecipe {
if (!user.tenantIds.contains(tenantId)) {
continue;
}
if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) {
if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUserId)) {
throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
user.getSupertokensUserId(),
"This user's email is already associated with another user ID");
@ -478,7 +470,7 @@ public class AuthRecipe {
if (!user.tenantIds.contains(tenantId)) {
continue;
}
if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) {
if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUserId)) {
throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
user.getSupertokensUserId(),
"This user's phone number is already associated with another user" +
@ -487,16 +479,16 @@ public class AuthRecipe {
}
}
if (currLoginMethod.thirdParty != null) {
if (currLoginMethod.thirdPartyId != null) {
List<AuthRecipeUserInfo> extraUsersWithThirdParty = allUsersWithExtraData.stream().filter(authRecipeUserInfo -> Arrays.stream(
authRecipeUserInfo.loginMethods).anyMatch(loginMethod1 -> loginMethod1.thirdParty != null)).collect(Collectors.toList());
for(AuthRecipeUserInfo extraUser : extraUsersWithThirdParty) {
if(extraUser.isPrimaryUser && extraUser.tenantIds.contains(tenantId)
&& !extraUser.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) {
&& !extraUser.getSupertokensUserId().equals(primaryUserId)) {
for (LoginMethod loginMethodExtra : extraUser.loginMethods) {
if (loginMethodExtra.thirdParty != null &&
loginMethodExtra.thirdParty.userId.equals(currLoginMethod.thirdParty.userId)
&& loginMethodExtra.thirdParty.id.equals(currLoginMethod.thirdParty.id)) {
loginMethodExtra.thirdParty.userId.equals(currLoginMethod.thirdPartyUserId)
&& loginMethodExtra.thirdParty.id.equals(currLoginMethod.thirdPartyId)) {
throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
extraUser.getSupertokensUserId(),
@ -517,7 +509,7 @@ public class AuthRecipe {
FeatureNotEnabledException, InputUserIdIsNotAPrimaryUserException,
RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException {
try {
return linkAccounts(main, new AppIdentifier(null, null),
return linkAccounts(main, ResourceDistributor.getAppForTesting().toAppIdentifier(),
StorageLayer.getStorage(main), recipeUserId, primaryUserId);
} catch (TenantOrAppNotFoundException e) {
throw new RuntimeException(e);
@ -588,10 +580,10 @@ public class AuthRecipe {
}
}
public static List<LinkAccountsBulkResult> linkMultipleAccounts(Main main, AppIdentifier appIdentifier,
Storage storage, Map<String, String> recipeUserIdToPrimaryUserId,
List<String> allDistinctEmailAddresses, List<String> allDistinctPhones,
Map<String, String> allThirdpartyUserIdsToThirdpartyIds)
public static void linkMultipleAccountsForBulkImport(Main main, AppIdentifier appIdentifier,
Storage storage,
List<BulkImportUser> users,
List<AuthRecipeUserInfo> usersWithSameExtraData)
throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException {
if (!Utils.isAccountLinkingEnabled(main, appIdentifier)) {
@ -603,20 +595,13 @@ public class AuthRecipe {
Map<String, Exception> errorByUserId = new HashMap<>();
try {
List<LinkAccountsBulkResult> linkAccountsResults = authRecipeStorage.startTransaction(con -> {
List<CanLinkAccountsBulkResult> canLinkAccounts = canLinkMultipleAccountsHelper(con, appIdentifier,
authRecipeStorage, recipeUserIdToPrimaryUserId, allDistinctEmailAddresses, allDistinctPhones,
allThirdpartyUserIdsToThirdpartyIds);
List<LinkAccountsBulkResult> results = new ArrayList<>();
authRecipeStorage.startTransaction(con -> {
List<CanLinkAccountsBulkResult> canLinkAccounts = canLinkMultipleAccountsHelperForBulkImport(con, appIdentifier,
authRecipeStorage, users, usersWithSameExtraData);
Map<String, String> recipeUserByPrimaryUserNeedsLinking = new HashMap<>();
if(!canLinkAccounts.isEmpty()){
for(CanLinkAccountsBulkResult canLinkAccountsBulkResult : canLinkAccounts) {
if (canLinkAccountsBulkResult.alreadyLinked) {
results.add(new LinkAccountsBulkResult(
canLinkAccountsBulkResult.authRecipeUserInfo, true, null));
} else if(canLinkAccountsBulkResult.error != null) {
results.add(new LinkAccountsBulkResult(
canLinkAccountsBulkResult.authRecipeUserInfo, false, canLinkAccountsBulkResult.error)); // preparing to return the error
if(!canLinkAccountsBulkResult.alreadyLinked && canLinkAccountsBulkResult.error != null) {
errorByUserId.put(canLinkAccountsBulkResult.recipeUserId, canLinkAccountsBulkResult.error);
} else {
recipeUserByPrimaryUserNeedsLinking.put(canLinkAccountsBulkResult.recipeUserId, canLinkAccountsBulkResult.primaryUserId);
@ -624,32 +609,14 @@ public class AuthRecipe {
}
// link the remaining
authRecipeStorage.linkMultipleAccounts_Transaction(appIdentifier, con, recipeUserByPrimaryUserNeedsLinking);
List<AuthRecipeUserInfo> linkedPrimaryUsers = getUsersById(appIdentifier, authRecipeStorage, new ArrayList<>(recipeUserByPrimaryUserNeedsLinking.values()));
for(AuthRecipeUserInfo linkedUser : linkedPrimaryUsers){
results.add(new LinkAccountsBulkResult(linkedUser, false, null));
}
authRecipeStorage.commitTransaction(con);
}
if(!errorByUserId.isEmpty()) {
throw new StorageQueryException(new BulkImportBatchInsertException("link accounts errors", errorByUserId));
}
return results;
return null;
});
for(LinkAccountsBulkResult result : linkAccountsResults) {
if (!result.wasAlreadyLinked) {
io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult =
io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(
appIdentifier, authRecipeStorage,
result.user.getSupertokensUserId(), UserIdType.SUPERTOKENS);
// finally, we revoke all sessions of the recipeUser Id cause their user ID has changed.
Session.revokeAllSessionsForUser(main, appIdentifier, authRecipeStorage,
mappingResult == null ? result.user.getSupertokensUserId() : mappingResult.externalUserId, false);
}
}
return linkAccountsResults;
} catch (StorageTransactionLogicException e) {
throw new StorageQueryException(e);
}
@ -666,11 +633,11 @@ public class AuthRecipe {
}
public static class LinkAccountsBulkResult {
public final AuthRecipeUserInfo user;
public final BulkImportUser user;
public final boolean wasAlreadyLinked;
public final Exception error;
public LinkAccountsBulkResult(AuthRecipeUserInfo user, boolean wasAlreadyLinked, Exception error) {
public LinkAccountsBulkResult(BulkImportUser user, boolean wasAlreadyLinked, Exception error) {
this.user = user;
this.wasAlreadyLinked = wasAlreadyLinked;
this.error = error;
@ -682,7 +649,7 @@ public class AuthRecipe {
String recipeUserId)
throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException,
RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, UnknownUserIdException {
return canCreatePrimaryUser(new AppIdentifier(null, null), StorageLayer.getStorage(main), recipeUserId);
return canCreatePrimaryUser(ResourceDistributor.getAppForTesting().toAppIdentifier(), StorageLayer.getStorage(main), recipeUserId);
}
public static CreatePrimaryUserResult canCreatePrimaryUser(AppIdentifier appIdentifier,
@ -799,116 +766,151 @@ public class AuthRecipe {
return new CreatePrimaryUserResult(targetUser, false);
}
private static List<CreatePrimaryUserBulkResult> canCreatePrimaryUsersHelper(TransactionConnection con,
AppIdentifier appIdentifier,
Storage storage,
List<String> recipeUserIds,
List<String> allDistinctEmails,
List<String> allPhones,
Map<String, String> thirdpartyUserIdToThirdpartyId)
private static CreatePrimaryUsersResultHolder canCreatePrimaryUsersHelperForBulkImport(TransactionConnection con,
AppIdentifier appIdentifier,
Storage storage,
List<BulkImportUser> bulkImportUsers)
throws StorageQueryException, UnknownUserIdException{
AuthRecipeSQLStorage authRecipeStorage = StorageUtils.getAuthRecipeStorage(storage);
List<AuthRecipeUserInfo> targetUsers = authRecipeStorage.getPrimaryUsersByIds_Transaction(appIdentifier, con,
recipeUserIds);
if (targetUsers == null || targetUsers.isEmpty()) {
if (bulkImportUsers == null || bulkImportUsers.isEmpty()) {
throw new UnknownUserIdException();
}
DistinctAuthIdentifiers mailPhoneThirdParty = getDistinctAuthIdentifiers(bulkImportUsers);
List<CreatePrimaryUserBulkResult> results = new ArrayList<>();
List<AuthRecipeUserInfo> allUsersWithProvidedExtraData =
List.of(authRecipeStorage.
listPrimaryUsersByMultipleEmailsOrPhoneNumbersOrThirdparty_Transaction(appIdentifier, con,
allDistinctEmails, allPhones, thirdpartyUserIdToThirdpartyId));
new ArrayList<>(mailPhoneThirdParty.allEmails), new ArrayList<>(mailPhoneThirdParty.allPhoneNumber),
mailPhoneThirdParty.allThirdParty)); // this is multiple - not so cheap DB query, but we need to do it
for(int i = 0; i < targetUsers.size(); i++) {
AuthRecipeUserInfo targetUser = targetUsers.get(i);
if (targetUser.isPrimaryUser) {
if (targetUser.getSupertokensUserId()
.equals(recipeUserIds.get(i))) {
results.add(new CreatePrimaryUserBulkResult(targetUser, true, null));
} else {
results.add(new CreatePrimaryUserBulkResult(targetUser, false,
new RecipeUserIdAlreadyLinkedWithPrimaryUserIdException(targetUser.getSupertokensUserId(),
"This user ID is already linked to another user ID")));
continue;
}
}
for (BulkImportUser targetUser : bulkImportUsers) {
BulkImportUser.LoginMethod primaryLoginMethod = BulkImportUserUtils.getPrimaryLoginMethod(targetUser);
// this means that the user has only one login method since it's not a primary user
// nor is it linked to a primary user
assert (targetUser.loginMethods.length == 1);
LoginMethod loginMethod = targetUser.loginMethods[0];
boolean errorFound = false;
for (String tenantId : targetUser.tenantIds) {
if (loginMethod.email != null) {
List<AuthRecipeUserInfo> usersWithSameEmail = allUsersWithProvidedExtraData.stream().filter(authRecipeUserInfo -> Arrays.stream(
authRecipeUserInfo.loginMethods).map(loginMethod1 -> loginMethod1.email).collect(Collectors.toList()).contains(loginMethod.email)).collect(
Collectors.toList());
for (AuthRecipeUserInfo user : usersWithSameEmail) {
if (!user.tenantIds.contains(tenantId)) {
continue;
}
if (user.isPrimaryUser) {
results.add(new CreatePrimaryUserBulkResult(targetUser, false,
new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
user.getSupertokensUserId(),
"This user's email is already associated with another user ID")));
errorFound = true;
break;
for (BulkImportUser.LoginMethod loginMethod : targetUser.loginMethods) {
// note here: account takeover risk checks are done in the sdk. The situation in which someone registers
// for example with a thirparty which also verifies email address and later someone else tries to register
// with the same email address but with emailpassword is not handled here. This is because the sdk
// will handle this. In the bulk import we have no means to check this.
boolean errorFound = false;
for (String tenantId : loginMethod.tenantIds) {
if (loginMethod.email != null) {
List<AuthRecipeUserInfo> usersWithSameEmail = allUsersWithProvidedExtraData.stream()
.filter(authRecipeUserInfo -> Arrays.stream(
authRecipeUserInfo.loginMethods).map(loginMethod1 -> loginMethod1.email)
.collect(Collectors.toList()).contains(loginMethod.email)).collect(
Collectors.toList());
for (AuthRecipeUserInfo user : usersWithSameEmail) {
if (!user.tenantIds.contains(tenantId)) {
continue;
}
if (user.isPrimaryUser) {
results.add(new CreatePrimaryUserBulkResult(targetUser, primaryLoginMethod, false,
new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
user.getSupertokensUserId(),
"This user's email is already associated with another user ID")));
errorFound = true;
break;
}
}
}
}
if (loginMethod.phoneNumber != null) {
List<AuthRecipeUserInfo> usersWithSamePhoneNumber = allUsersWithProvidedExtraData.stream().filter(authRecipeUserInfo -> Arrays.stream(
authRecipeUserInfo.loginMethods).map(loginMethod1 -> loginMethod1.phoneNumber).collect(Collectors.toList()).contains(loginMethod.phoneNumber)).collect(
Collectors.toList());
for (AuthRecipeUserInfo user : usersWithSamePhoneNumber) {
if (!user.tenantIds.contains(tenantId)) {
continue;
}
if (user.isPrimaryUser) {
results.add(new CreatePrimaryUserBulkResult(targetUser, false,
new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
user.getSupertokensUserId(),
"This user's phone number is already associated with another user" +
" ID")));
errorFound = true;
break;
if (loginMethod.phoneNumber != null) {
List<AuthRecipeUserInfo> usersWithSamePhoneNumber = allUsersWithProvidedExtraData.stream()
.filter(authRecipeUserInfo -> Arrays.stream(
authRecipeUserInfo.loginMethods).map(loginMethod1 -> loginMethod1.phoneNumber)
.collect(Collectors.toList()).contains(loginMethod.phoneNumber)).collect(
Collectors.toList());
for (AuthRecipeUserInfo user : usersWithSamePhoneNumber) {
if (!user.tenantIds.contains(tenantId)) {
continue;
}
if (user.isPrimaryUser) {
results.add(new CreatePrimaryUserBulkResult(targetUser, primaryLoginMethod, false,
new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
user.getSupertokensUserId(),
"This user's phone number is already associated with another user" +
" ID")));
errorFound = true;
break;
}
}
}
}
if (loginMethod.thirdParty != null) {
List<AuthRecipeUserInfo> extraUsersWithThirdParty = allUsersWithProvidedExtraData.stream().filter(authRecipeUserInfo -> Arrays.stream(
authRecipeUserInfo.loginMethods).anyMatch(loginMethod1 -> loginMethod1.thirdParty != null)).collect(Collectors.toList());
for(AuthRecipeUserInfo extraUser : extraUsersWithThirdParty) {
if(extraUser.isPrimaryUser && extraUser.tenantIds.contains(tenantId)) {
for (LoginMethod loginMethodExtra : extraUser.loginMethods) {
if (loginMethodExtra.thirdParty != null &&
loginMethodExtra.thirdParty.userId.equals(loginMethod.thirdParty.userId)
&& loginMethodExtra.thirdParty.id.equals(loginMethod.thirdParty.id)) {
if (loginMethod.thirdPartyId != null && loginMethod.thirdPartyUserId != null) {
List<AuthRecipeUserInfo> extraUsersWithThirdParty = allUsersWithProvidedExtraData.stream()
.filter(authRecipeUserInfo -> Arrays.stream(
authRecipeUserInfo.loginMethods)
.anyMatch(loginMethod1 -> loginMethod1.thirdParty != null))
.collect(Collectors.toList());
for (AuthRecipeUserInfo extraUser : extraUsersWithThirdParty) {
if (extraUser.isPrimaryUser && extraUser.tenantIds.contains(tenantId)) {
for (LoginMethod loginMethodExtra : extraUser.loginMethods) {
if (loginMethodExtra.thirdParty != null &&
loginMethodExtra.thirdParty.userId.equals(loginMethod.thirdPartyUserId)
&& loginMethodExtra.thirdParty.id.equals(loginMethod.thirdPartyId)) {
results.add(new CreatePrimaryUserBulkResult(targetUser, false,
new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
extraUser.getSupertokensUserId(),
"This user's third party login is already associated with another" +
" user ID")));
errorFound = true;
break;
results.add(
new CreatePrimaryUserBulkResult(targetUser, primaryLoginMethod, false,
new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
extraUser.getSupertokensUserId(),
"This user's third party login is already associated with another" +
" user ID")));
errorFound = true;
break;
}
}
}
}
}
}
if(!errorFound){
results.add(new CreatePrimaryUserBulkResult(targetUser, false, null));
if (!errorFound) {
results.add(new CreatePrimaryUserBulkResult(targetUser, primaryLoginMethod, false, null));
}
}
}
}
return results;
CreatePrimaryUsersResultHolder resultHolder = new CreatePrimaryUsersResultHolder();
resultHolder.createPrimaryUserBulkResults = results;
resultHolder.usersWithSameExtraData = allUsersWithProvidedExtraData;
return resultHolder;
}
@NotNull
private static DistinctAuthIdentifiers getDistinctAuthIdentifiers(List<BulkImportUser> bulkImportUsers) {
Set<String> allEmails = new HashSet<>();
Set<String> allPhoneNumber = new HashSet<>();
Map<String, String> allThirdParty = new HashMap<>();
for (BulkImportUser user : bulkImportUsers) {
for (BulkImportUser.LoginMethod loginMethod : user.loginMethods) {
if (loginMethod.email != null) {
allEmails.add(loginMethod.email);
}
if (loginMethod.phoneNumber != null) {
allPhoneNumber.add(loginMethod.phoneNumber);
}
if (loginMethod.thirdPartyId != null && loginMethod.thirdPartyUserId != null) {
allThirdParty.put(loginMethod.thirdPartyUserId, loginMethod.thirdPartyId);
}
}
}
DistinctAuthIdentifiers mailPhoneThirdparty = new DistinctAuthIdentifiers(allEmails, allPhoneNumber, allThirdParty);
return mailPhoneThirdparty;
}
private static class DistinctAuthIdentifiers {
public final Set<String> allEmails;
public final Set<String> allPhoneNumber;
public final Map<String, String> allThirdParty;
public DistinctAuthIdentifiers(Set<String> allEmails, Set<String> allPhoneNumber, Map<String, String> allThirdParty) {
this.allEmails = allEmails;
this.allPhoneNumber = allPhoneNumber;
this.allThirdParty = allThirdParty;
}
}
@ -919,7 +921,7 @@ public class AuthRecipe {
RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, UnknownUserIdException,
FeatureNotEnabledException {
try {
return createPrimaryUser(main, new AppIdentifier(null, null), StorageLayer.getStorage(main), recipeUserId);
return createPrimaryUser(main, ResourceDistributor.getAppForTesting().toAppIdentifier(), StorageLayer.getStorage(main), recipeUserId);
} catch (TenantOrAppNotFoundException e) {
throw new RuntimeException(e);
}
@ -973,15 +975,19 @@ public class AuthRecipe {
}
}
public static List<CreatePrimaryUserBulkResult> createPrimaryUsers(Main main,
AppIdentifier appIdentifier,
Storage storage,
List<String> recipeUserIds,
List<String> allDistinctEmails,
List<String> allDistinctPhones,
Map<String, String> thirdpartyUserIdsToThirdpartyIds)
//helper class to return together the results of primary user creation and the users with the same extradata (email, phone, etc)
public static class CreatePrimaryUsersResultHolder {
public List<CreatePrimaryUserBulkResult> createPrimaryUserBulkResults;
public List<AuthRecipeUserInfo> usersWithSameExtraData;
}
public static CreatePrimaryUsersResultHolder createPrimaryUsersForBulkImport(Main main,
AppIdentifier appIdentifier,
Storage storage,
List<BulkImportUser> bulkImportUsers)
throws StorageQueryException, TenantOrAppNotFoundException,
FeatureNotEnabledException {
if (!Utils.isAccountLinkingEnabled(main, appIdentifier)) {
throw new FeatureNotEnabledException(
"Account linking feature is not enabled for this app. Please contact support to enable it.");
@ -993,21 +999,22 @@ public class AuthRecipe {
return authRecipeStorage.startTransaction(con -> {
try {
List<CreatePrimaryUserBulkResult> results = canCreatePrimaryUsersHelper(con, appIdentifier, authRecipeStorage,
recipeUserIds, allDistinctEmails, allDistinctPhones, thirdpartyUserIdsToThirdpartyIds);
CreatePrimaryUsersResultHolder resultHolder = canCreatePrimaryUsersHelperForBulkImport(con, appIdentifier, authRecipeStorage,
bulkImportUsers);
List<CreatePrimaryUserBulkResult> results = resultHolder.createPrimaryUserBulkResults;
List<CreatePrimaryUserBulkResult> canMakePrimaryUsers = new ArrayList<>();
for(CreatePrimaryUserBulkResult result : results) {
if (result.wasAlreadyAPrimaryUser) {
continue;
}
if(result.error != null) {
errorsByUserId.put(result.user.getSupertokensUserId(), result.error);
errorsByUserId.put(result.user.id, result.error);
continue;
}
canMakePrimaryUsers.add(result);
}
authRecipeStorage.makePrimaryUsers_Transaction(appIdentifier, con,
canMakePrimaryUsers.stream().map(canMakePrimaryUser -> canMakePrimaryUser.user.getSupertokensUserId()).collect(
canMakePrimaryUsers.stream().map(canMakePrimaryUser -> canMakePrimaryUser.user.id).collect(
Collectors.toList()));
authRecipeStorage.commitTransaction(con);
@ -1017,17 +1024,18 @@ public class AuthRecipe {
continue;
}
if(result.error != null) {
errorsByUserId.put(result.user.getSupertokensUserId(), result.error);
errorsByUserId.put(result.user.id, result.error);
continue;
}
result.user.isPrimaryUser = true;
result.primaryLoginMethod.isPrimary = true;
result.user.primaryUserId = result.primaryLoginMethod.superTokensUserId;
}
if(!errorsByUserId.isEmpty()) {
throw new StorageTransactionLogicException(new BulkImportBatchInsertException("create primary users errors", errorsByUserId));
}
return results;
return resultHolder;
} catch (UnknownUserIdException e) {
throw new StorageTransactionLogicException(e);
}
@ -1037,76 +1045,112 @@ public class AuthRecipe {
}
}
public static AuthRecipeUserInfo[] getUsersByAccountInfo(TenantIdentifier tenantIdentifier,
Storage storage,
boolean doUnionOfAccountInfo, String email,
String phoneNumber, String thirdPartyId,
String thirdPartyUserId)
String thirdPartyUserId,
String webauthnCredentialId)
throws StorageQueryException {
Set<AuthRecipeUserInfo> result = loadAuthRecipeUserInfosByVariousIds(
tenantIdentifier, storage, email, phoneNumber, thirdPartyId, thirdPartyUserId, webauthnCredentialId);
if (doUnionOfAccountInfo) {
return mergeAuthRecipeUserInfosResultWithORMatch(result); // matches any of the provided: email, thirdparty, phone number, webauthnCredential
} else {
return mergeAuthRecipeUserInfosResultWithANDMatch(email, phoneNumber, thirdPartyId, thirdPartyUserId, webauthnCredentialId,
result); // matches all the provided: email, thirdparty, phone number, webauthnCredential
}
}
private static AuthRecipeUserInfo[] mergeAuthRecipeUserInfosResultWithANDMatch(String email, String phoneNumber,
String thirdPartyId, String thirdPartyUserId,
String webauthnCredentialId,
Set<AuthRecipeUserInfo> result) {
List<AuthRecipeUserInfo> finalList = new ArrayList<>();
for (AuthRecipeUserInfo user : result) {
boolean emailMatch = email == null;
boolean phoneNumberMatch = phoneNumber == null;
boolean thirdPartyMatch = thirdPartyId == null;
boolean webauthnCredentialIdMatch = webauthnCredentialId == null;
for (LoginMethod lM : user.loginMethods) {
if (email != null && email.equals(lM.email)) {
emailMatch = true;
}
if (phoneNumber != null && phoneNumber.equals(lM.phoneNumber)) {
phoneNumberMatch = true;
}
if (thirdPartyId != null &&
(new LoginMethod.ThirdParty(thirdPartyId, thirdPartyUserId)).equals(lM.thirdParty)) {
thirdPartyMatch = true;
}
if(webauthnCredentialId != null
&& lM.webauthN != null
&& lM.webauthN.credentialIds.contains(webauthnCredentialId)){
webauthnCredentialIdMatch = true;
}
}
if (emailMatch && phoneNumberMatch && thirdPartyMatch && webauthnCredentialIdMatch) {
finalList.add(user);
}
}
finalList.sort((o1, o2) -> {
if (o1.timeJoined < o2.timeJoined) {
return -1;
} else if (o1.timeJoined > o2.timeJoined) {
return 1;
}
return 0;
});
return finalList.toArray(new AuthRecipeUserInfo[0]);
}
private static AuthRecipeUserInfo[] mergeAuthRecipeUserInfosResultWithORMatch(Set<AuthRecipeUserInfo> result) {
AuthRecipeUserInfo[] finalResult = result.toArray(new AuthRecipeUserInfo[0]);
return Arrays.stream(finalResult).sorted((o1, o2) -> {
if (o1.timeJoined < o2.timeJoined) {
return -1;
} else if (o1.timeJoined > o2.timeJoined) {
return 1;
}
return 0;
}).toArray(AuthRecipeUserInfo[]::new);
}
@NotNull
private static Set<AuthRecipeUserInfo> loadAuthRecipeUserInfosByVariousIds(TenantIdentifier tenantIdentifier, Storage storage,
String email, String phoneNumber, String thirdPartyId,
String thirdPartyUserId, String webauthnCredentialId)
throws StorageQueryException {
Set<AuthRecipeUserInfo> result = new HashSet<>();
AuthRecipeSQLStorage authRecipeStorage = StorageUtils.getAuthRecipeStorage(storage);
if (email != null) {
AuthRecipeUserInfo[] users = StorageUtils.getAuthRecipeStorage(storage)
AuthRecipeUserInfo[] users = authRecipeStorage
.listPrimaryUsersByEmail(tenantIdentifier, email);
result.addAll(List.of(users));
}
if (phoneNumber != null) {
AuthRecipeUserInfo[] users = StorageUtils.getAuthRecipeStorage(storage)
AuthRecipeUserInfo[] users = authRecipeStorage
.listPrimaryUsersByPhoneNumber(tenantIdentifier, phoneNumber);
result.addAll(List.of(users));
}
if (thirdPartyId != null && thirdPartyUserId != null) {
AuthRecipeUserInfo user = StorageUtils.getAuthRecipeStorage(storage)
AuthRecipeUserInfo user = authRecipeStorage
.getPrimaryUserByThirdPartyInfo(tenantIdentifier, thirdPartyId, thirdPartyUserId);
if (user != null) {
result.add(user);
}
}
if (doUnionOfAccountInfo) {
AuthRecipeUserInfo[] finalResult = result.toArray(new AuthRecipeUserInfo[0]);
return Arrays.stream(finalResult).sorted((o1, o2) -> {
if (o1.timeJoined < o2.timeJoined) {
return -1;
} else if (o1.timeJoined > o2.timeJoined) {
return 1;
}
return 0;
}).toArray(AuthRecipeUserInfo[]::new);
} else {
List<AuthRecipeUserInfo> finalList = new ArrayList<>();
for (AuthRecipeUserInfo user : result) {
boolean emailMatch = email == null;
boolean phoneNumberMatch = phoneNumber == null;
boolean thirdPartyMatch = thirdPartyId == null;
for (LoginMethod lM : user.loginMethods) {
if (email != null && email.equals(lM.email)) {
emailMatch = true;
}
if (phoneNumber != null && phoneNumber.equals(lM.phoneNumber)) {
phoneNumberMatch = true;
}
if (thirdPartyId != null &&
(new LoginMethod.ThirdParty(thirdPartyId, thirdPartyUserId)).equals(lM.thirdParty)) {
thirdPartyMatch = true;
}
}
if (emailMatch && phoneNumberMatch && thirdPartyMatch) {
finalList.add(user);
}
if(webauthnCredentialId != null){
AuthRecipeUserInfo user = authRecipeStorage
.getPrimaryUserByWebauthNCredentialId(tenantIdentifier, webauthnCredentialId);
if (user != null) {
result.add(user);
}
finalList.sort((o1, o2) -> {
if (o1.timeJoined < o2.timeJoined) {
return -1;
} else if (o1.timeJoined > o2.timeJoined) {
return 1;
}
return 0;
});
return finalList.toArray(new AuthRecipeUserInfo[0]);
}
return result;
}
public static long getUsersCountForTenant(TenantIdentifier tenantIdentifier,
@ -1138,7 +1182,7 @@ public class AuthRecipe {
RECIPE_ID[] includeRecipeIds) throws StorageQueryException {
try {
Storage storage = StorageLayer.getStorage(main);
return getUsersCountForTenant(TenantIdentifier.BASE_TENANT, storage, includeRecipeIds);
return getUsersCountForTenant(ResourceDistributor.getAppForTesting(), storage, includeRecipeIds);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
}
@ -1188,7 +1232,7 @@ public class AuthRecipe {
throws StorageQueryException, UserPaginationToken.InvalidTokenException {
try {
Storage storage = StorageLayer.getStorage(main);
return getUsers(TenantIdentifier.BASE_TENANT, storage,
return getUsers(ResourceDistributor.getAppForTesting(), storage,
limit, timeJoinedOrder, paginationToken, includeRecipeIds, dashboardSearchTags);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -1346,7 +1390,7 @@ public class AuthRecipe {
public static void deleteUser(Main main, String userId, boolean removeAllLinkedAccounts)
throws StorageQueryException, StorageTransactionLogicException {
Storage storage = StorageLayer.getStorage(main);
AppIdentifier appIdentifier = new AppIdentifier(null, null);
AppIdentifier appIdentifier = ResourceDistributor.getAppForTesting().toAppIdentifier();
UserIdMapping mapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(appIdentifier,
storage, userId, UserIdType.ANY);
@ -1357,7 +1401,7 @@ public class AuthRecipe {
public static void deleteUser(Main main, String userId)
throws StorageQueryException, StorageTransactionLogicException {
Storage storage = StorageLayer.getStorage(main);
AppIdentifier appIdentifier = new AppIdentifier(null, null);
AppIdentifier appIdentifier = ResourceDistributor.getAppForTesting().toAppIdentifier();
UserIdMapping mapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(appIdentifier,
storage, userId, UserIdType.ANY);

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2025, 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.authRecipe.exception;
public class BulkImportRecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException extends Exception {
public final String recipeUserId;
public BulkImportRecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(String recipeUserId) {
super("The recipe user id '" + recipeUserId + "' is already linked with another primary user id");
this.recipeUserId = recipeUserId;
}
}

View File

@ -32,6 +32,7 @@ import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithEmailAlreadyExistsException;
import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithPhoneNumberAlreadyExistsException;
import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException;
import io.supertokens.output.Logging;
import io.supertokens.passwordless.Passwordless;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.StorageUtils;
@ -73,6 +74,7 @@ import io.supertokens.usermetadata.UserMetadata;
import io.supertokens.userroles.UserRoles;
import io.supertokens.utils.Utils;
import jakarta.servlet.ServletException;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -94,8 +96,6 @@ public class BulkImport {
public static final int GET_USERS_DEFAULT_LIMIT = 100;
// Maximum number of users that can be deleted in a single operation
public static final int DELETE_USERS_MAX_LIMIT = 500;
// Number of users to process in a single batch of ProcessBulkImportUsers Cron Job
public static final int PROCESS_USERS_BATCH_SIZE = 8000;
// Time interval in seconds between two consecutive runs of ProcessBulkImportUsers Cron Job
public static final int PROCESS_USERS_INTERVAL_SECONDS = 5*60; // 5 minutes
private static final Logger log = LoggerFactory.getLogger(BulkImport.class);
@ -173,7 +173,7 @@ public class BulkImport {
SQLStorage bulkImportProxyStorage = (SQLStorage) getBulkImportProxyStorage(main, firstTenantIdentifier);
LoginMethod primaryLM = getPrimaryLoginMethod(user);
LoginMethod primaryLM = BulkImportUserUtils.getPrimaryLoginMethod(user);
try {
return bulkImportProxyStorage.startTransaction(con -> {
@ -210,13 +210,28 @@ public class BulkImport {
Storage bulkImportProxyStorage, List<BulkImportUser> users, Storage[] allStoragesForApp)
throws StorageTransactionLogicException {
try {
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing login methods..");
processUsersLoginMethods(main, appIdentifier, bulkImportProxyStorage, users);
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing login methods DONE");
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating Primary users and linking accounts..");
createPrimaryUsersAndLinkAccounts(main, appIdentifier, bulkImportProxyStorage, users);
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating Primary users and linking accounts DONE");
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating user id mappings..");
createMultipleUserIdMapping(appIdentifier, users, allStoragesForApp);
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating user id mappings DONE");
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Verifying email addresses..");
verifyMultipleEmailForAllLoginMethods(appIdentifier, bulkImportProxyStorage, users);
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Verifying email addresses DONE");
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating TOTP devices..");
createMultipleTotpDevices(main, appIdentifier, bulkImportProxyStorage, users);
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating TOTP devices DONE");
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating user metadata..");
createMultipleUserMetadata(appIdentifier, bulkImportProxyStorage, users);
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating user metadata DONE");
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating user roles..");
createMultipleUserRoles(main, appIdentifier, bulkImportProxyStorage, users);
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating user roles DONE");
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Effective processUsersImportSteps DONE");
} catch ( StorageQueryException | FeatureNotEnabledException |
TenantOrAppNotFoundException e) {
throw new StorageTransactionLogicException(e);
@ -226,6 +241,7 @@ public class BulkImport {
public static void processUsersLoginMethods(Main main, AppIdentifier appIdentifier, Storage storage,
List<BulkImportUser> users) throws StorageTransactionLogicException {
//sort login methods together
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Sorting login methods by recipeId..");
Map<String, List<LoginMethod>> sortedLoginMethods = new HashMap<>();
for (BulkImportUser user: users) {
for(LoginMethod loginMethod : user.loginMethods){
@ -237,19 +253,25 @@ public class BulkImport {
}
List<ImportUserBase> importedUsers = new ArrayList<>();
if (sortedLoginMethods.containsKey("emailpassword")) {
importedUsers.addAll(
processEmailPasswordLoginMethods(main, storage, sortedLoginMethods.get("emailpassword"),
if (sortedLoginMethods.containsKey("emailpassword")) {
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing emailpassword login methods..");
importedUsers.addAll(
processEmailPasswordLoginMethods(main, storage, sortedLoginMethods.get("emailpassword"),
appIdentifier));
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing emailpassword login methods DONE");
}
if (sortedLoginMethods.containsKey("thirdparty")) {
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing thirdparty login methods..");
importedUsers.addAll(
processThirdpartyLoginMethods(main, storage, sortedLoginMethods.get("thirdparty"),
appIdentifier));
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing thirdparty login methods DONE");
}
if (sortedLoginMethods.containsKey("passwordless")) {
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing passwordless login methods..");
importedUsers.addAll(processPasswordlessLoginMethods(main, appIdentifier, storage,
sortedLoginMethods.get("passwordless")));
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing passwordless login methods DONE");
}
Set<String> actualKeys = new HashSet<>(sortedLoginMethods.keySet());
List.of("emailpassword", "thirdparty", "passwordless").forEach(actualKeys::remove);
@ -280,20 +302,18 @@ public class BulkImport {
try {
List<PasswordlessImportUser> usersToImport = new ArrayList<>();
for (LoginMethod loginMethod : loginMethods) {
String userId = Utils.getUUID();
TenantIdentifier tenantIdentifierForLoginMethod = new TenantIdentifier(
appIdentifier.getConnectionUriDomain(),
appIdentifier.getAppId(), loginMethod.tenantIds.get(
0)); // the cron runs per app. The app stays the same, the tenant can change
usersToImport.add(new PasswordlessImportUser(userId, loginMethod.phoneNumber,
usersToImport.add(new PasswordlessImportUser(loginMethod.superTokensUserId, loginMethod.phoneNumber,
loginMethod.email, tenantIdentifierForLoginMethod, loginMethod.timeJoinedInMSSinceEpoch));
loginMethod.superTokensUserId = userId;
}
Passwordless.createPasswordlessUsers(storage, usersToImport);
return usersToImport;
} catch (StorageQueryException | StorageTransactionLogicException e) {
Logging.debug(main, TenantIdentifier.BASE_TENANT, "exception: " + e.getMessage());
if (e.getCause() instanceof BulkImportBatchInsertException) {
Map<String, Exception> errorsByPosition = ((BulkImportBatchInsertException) e.getCause()).exceptionByUserId;
for (String userid : errorsByPosition.keySet()) {
@ -327,13 +347,11 @@ public class BulkImport {
try {
List<ThirdPartyImportUser> usersToImport = new ArrayList<>();
for (LoginMethod loginMethod: loginMethods){
String userId = Utils.getUUID();
TenantIdentifier tenantIdentifierForLoginMethod = new TenantIdentifier(appIdentifier.getConnectionUriDomain(),
appIdentifier.getAppId(), loginMethod.tenantIds.get(0)); // the cron runs per app. The app stays the same, the tenant can change
usersToImport.add(new ThirdPartyImportUser(loginMethod.email, userId, loginMethod.thirdPartyId,
usersToImport.add(new ThirdPartyImportUser(loginMethod.email, loginMethod.superTokensUserId, loginMethod.thirdPartyId,
loginMethod.thirdPartyUserId, tenantIdentifierForLoginMethod, loginMethod.timeJoinedInMSSinceEpoch));
loginMethod.superTokensUserId = userId;
}
ThirdParty.createMultipleThirdPartyUsers(storage, usersToImport);
@ -380,10 +398,8 @@ public class BulkImport {
.createHashWithSalt(tenantIdentifierForLoginMethod.toAppIdentifier(), emailPasswordLoginMethod.plainTextPassword);
}
emailPasswordLoginMethod.passwordHash = passwordHash;
String userId = Utils.getUUID();
usersToImport.add(new EmailPasswordImportUser(userId, emailPasswordLoginMethod.email,
usersToImport.add(new EmailPasswordImportUser(emailPasswordLoginMethod.superTokensUserId, emailPasswordLoginMethod.email,
emailPasswordLoginMethod.passwordHash, tenantIdentifierForLoginMethod, emailPasswordLoginMethod.timeJoinedInMSSinceEpoch));
emailPasswordLoginMethod.superTokensUserId = userId;
}
EmailPassword.createMultipleUsersWithPasswordHash(storage, usersToImport);
@ -419,7 +435,7 @@ public class BulkImport {
TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifier.getConnectionUriDomain(),
appIdentifier.getAppId(), tenantId);
Multitenancy.addUserIdToTenant(main, tenantIdentifier, storage, lm.getSuperTokenOrExternalUserId());
Multitenancy.addUserIdToTenant(main, tenantIdentifier, storage, lm.superTokensUserId);
} catch (TenantOrAppNotFoundException e) {
throw new StorageTransactionLogicException(new Exception("E009: " + e.getMessage()));
} catch (StorageQueryException e) {
@ -465,31 +481,15 @@ public class BulkImport {
List<BulkImportUser> users)
throws StorageTransactionLogicException, StorageQueryException, FeatureNotEnabledException,
TenantOrAppNotFoundException {
List<String> userIds =
users.stream()
.map(bulkImportUser -> getPrimaryLoginMethod(bulkImportUser).getSuperTokenOrExternalUserId())
.collect(Collectors.toList());
Set<String> allEmails = new HashSet<>();
Set<String> allPhoneNumber = new HashSet<>();
Map<String, String> allThirdParty = new HashMap<>();
for (BulkImportUser user : users) {
for (LoginMethod loginMethod : user.loginMethods) {
if (loginMethod.email != null) {
allEmails.add(loginMethod.email);
}
if (loginMethod.phoneNumber != null) {
allPhoneNumber.add(loginMethod.phoneNumber);
}
if (loginMethod.thirdPartyId != null && loginMethod.thirdPartyUserId != null) {
allThirdParty.put(loginMethod.thirdPartyUserId, loginMethod.thirdPartyId);
}
}
List<BulkImportUser> usersForAccountLinking = filterUsersInNeedOfAccountLinking(users);
if(usersForAccountLinking.isEmpty()){
return;
}
AuthRecipe.CreatePrimaryUsersResultHolder resultHolder;
try {
AuthRecipe.createPrimaryUsers(main, appIdentifier, storage, userIds, new ArrayList<>(allEmails),
new ArrayList<>(allPhoneNumber), allThirdParty);
resultHolder = AuthRecipe.createPrimaryUsersForBulkImport(main, appIdentifier, storage, usersForAccountLinking);
} catch (StorageQueryException e) {
if(e.getCause() instanceof BulkImportBatchInsertException){
Map<String, Exception> errorsByPosition = ((BulkImportBatchInsertException) e.getCause()).exceptionByUserId;
@ -521,27 +521,34 @@ public class BulkImport {
} catch (FeatureNotEnabledException e) {
throw new StorageTransactionLogicException(new Exception("E019: " + e.getMessage()));
}
if(resultHolder != null && resultHolder.usersWithSameExtraData != null){
linkAccountsForMultipleUser(main, appIdentifier, storage, usersForAccountLinking, resultHolder.usersWithSameExtraData);
}
}
linkAccountsForMultipleUser(main, appIdentifier, storage, users, new ArrayList<>(allEmails),
new ArrayList<>(allPhoneNumber), allThirdParty);
private static List<BulkImportUser> filterUsersInNeedOfAccountLinking(List<BulkImportUser> allUsers) {
if (allUsers == null || allUsers.isEmpty()) {
return Collections.emptyList();
}
return allUsers.stream().filter(bulkImportUser -> bulkImportUser.loginMethods.stream()
.anyMatch(loginMethod -> loginMethod.isPrimary) || bulkImportUser.loginMethods.size() > 1)
.collect(Collectors.toList());
}
private static void linkAccountsForMultipleUser(Main main, AppIdentifier appIdentifier, Storage storage,
List<BulkImportUser> users,
List<String> allDistinctEmails,
List<String> allDistinctPhones,
Map<String, String> thirdpartyUserIdsToThirdpartyIds)
List<BulkImportUser> users, List<AuthRecipeUserInfo> allUsersWithSameExtraData)
throws StorageTransactionLogicException {
Map<String, String> recipeUserIdByPrimaryUserId = collectRecipeIdsToPrimaryIds(users);
try {
AuthRecipe.linkMultipleAccounts(main, appIdentifier, storage, recipeUserIdByPrimaryUserId,
allDistinctEmails, allDistinctPhones, thirdpartyUserIdsToThirdpartyIds);
AuthRecipe.linkMultipleAccountsForBulkImport(main, appIdentifier, storage,
users, allUsersWithSameExtraData);
} catch (TenantOrAppNotFoundException e) {
throw new StorageTransactionLogicException(new Exception("E023: " + e.getMessage()));
} catch (FeatureNotEnabledException e) {
throw new StorageTransactionLogicException(new Exception("E024: " + e.getMessage()));
} catch (StorageQueryException e) {
if (e.getCause() instanceof BulkImportBatchInsertException) {
Map<String, String> recipeUserIdByPrimaryUserId = BulkImportUserUtils.collectRecipeIdsToPrimaryIds(users);
Map<String, Exception> errorByPosition = ((BulkImportBatchInsertException) e.getCause()).exceptionByUserId;
for (String userId : errorByPosition.keySet()) {
Exception currentException = errorByPosition.get(userId);
@ -575,37 +582,23 @@ public class BulkImport {
}
}
private static Map<String, String> collectRecipeIdsToPrimaryIds(List<BulkImportUser> users) {
Map<String, String> recipeUserIdByPrimaryUserId = new HashMap<>();
for(BulkImportUser user: users){
LoginMethod primaryLM = getPrimaryLoginMethod(user);
for (LoginMethod lm : user.loginMethods) {
if (lm.getSuperTokenOrExternalUserId().equals(primaryLM.getSuperTokenOrExternalUserId())) {
continue;
}
recipeUserIdByPrimaryUserId.put(lm.getSuperTokenOrExternalUserId(),
primaryLM.getSuperTokenOrExternalUserId());
}
}
return recipeUserIdByPrimaryUserId;
}
public static void createMultipleUserIdMapping(AppIdentifier appIdentifier,
List<BulkImportUser> users, Storage[] storages) throws StorageTransactionLogicException {
Map<String, String> superTokensUserIdToExternalUserId = new HashMap<>();
for(BulkImportUser user: users) {
if(user.externalUserId != null) {
LoginMethod primaryLoginMethod = getPrimaryLoginMethod(user);
LoginMethod primaryLoginMethod = BulkImportUserUtils.getPrimaryLoginMethod(user);
superTokensUserIdToExternalUserId.put(primaryLoginMethod.superTokensUserId, user.externalUserId);
primaryLoginMethod.externalUserId = user.externalUserId;
}
}
try {
List<UserIdMapping.UserIdBulkMappingResult> mappingResults = UserIdMapping.createMultipleUserIdMappings(
appIdentifier, storages,
superTokensUserIdToExternalUserId,
false, true);
if(!superTokensUserIdToExternalUserId.isEmpty()) {
List<UserIdMapping.UserIdBulkMappingResult> mappingResults = UserIdMapping.createMultipleUserIdMappings(
appIdentifier, storages,
superTokensUserIdToExternalUserId,
false, true);
}
} catch (StorageQueryException e) {
if(e.getCause() instanceof BulkImportBatchInsertException) {
Map<String, Exception> errorsByPosition = ((BulkImportBatchInsertException) e.getCause()).exceptionByUserId;
@ -637,12 +630,14 @@ public class BulkImport {
Map<String, JsonObject> usersMetadata = new HashMap<>();
for(BulkImportUser user: users) {
if (user.userMetadata != null) {
usersMetadata.put(getPrimaryLoginMethod(user).getSuperTokenOrExternalUserId(), user.userMetadata);
usersMetadata.put(BulkImportUserUtils.getPrimaryLoginMethod(user).getSuperTokenOrExternalUserId(), user.userMetadata);
}
}
try {
UserMetadata.updateMultipleUsersMetadata(appIdentifier, storage, usersMetadata);
if(!usersMetadata.isEmpty()) {
UserMetadata.updateMultipleUsersMetadata(appIdentifier, storage, usersMetadata);
}
} catch (TenantOrAppNotFoundException e) {
throw new StorageTransactionLogicException(new Exception("E040: " + e.getMessage()));
} catch (StorageQueryException e) {
@ -652,27 +647,7 @@ public class BulkImport {
public static void createMultipleUserRoles(Main main, AppIdentifier appIdentifier, Storage storage,
List<BulkImportUser> users) throws StorageTransactionLogicException {
Map<TenantIdentifier, Map<String, List<String>>> rolesToUserByTenant = new HashMap<>();
for (BulkImportUser user : users) {
if (user.userRoles != null) {
for (UserRole userRole : user.userRoles) {
for (String tenantId : userRole.tenantIds) {
TenantIdentifier tenantIdentifier = new TenantIdentifier(
appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(),
tenantId);
if(!rolesToUserByTenant.containsKey(tenantIdentifier)){
rolesToUserByTenant.put(tenantIdentifier, new HashMap<>());
}
if(!rolesToUserByTenant.get(tenantIdentifier).containsKey(user.externalUserId)){
rolesToUserByTenant.get(tenantIdentifier).put(user.externalUserId, new ArrayList<>());
}
rolesToUserByTenant.get(tenantIdentifier).get(user.externalUserId).add(userRole.role);
}
}
}
}
Map<TenantIdentifier, Map<String, List<String>>> rolesToUserByTenant = gatherRolesForUsersByTenant(appIdentifier, users);
try {
if(!rolesToUserByTenant.isEmpty()){
UserRoles.addMultipleRolesToMultipleUsers(main, appIdentifier, storage, rolesToUserByTenant);
@ -698,34 +673,94 @@ public class BulkImport {
}
private static Map<TenantIdentifier, Map<String, List<String>>> gatherRolesForUsersByTenant(AppIdentifier appIdentifier, List<BulkImportUser> users) {
Map<TenantIdentifier, Map<String, List<String>>> rolesToUserByTenant = new HashMap<>();
for (BulkImportUser user : users) {
if (user.userRoles != null) {
for (UserRole userRole : user.userRoles) {
for (String tenantId : userRole.tenantIds) {
TenantIdentifier tenantIdentifier = new TenantIdentifier(
appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(),
tenantId);
if(!rolesToUserByTenant.containsKey(tenantIdentifier)){
rolesToUserByTenant.put(tenantIdentifier, new HashMap<>());
}
String userIdToUse = user.externalUserId != null ?
user.externalUserId : user.id;
if(!rolesToUserByTenant.get(tenantIdentifier).containsKey(userIdToUse)){
rolesToUserByTenant.get(tenantIdentifier).put(userIdToUse, new ArrayList<>());
}
rolesToUserByTenant.get(tenantIdentifier).get(userIdToUse).add(userRole.role);
}
}
}
}
return rolesToUserByTenant;
}
public static void verifyMultipleEmailForAllLoginMethods(AppIdentifier appIdentifier, Storage storage,
List<BulkImportUser> users)
throws StorageTransactionLogicException {
Map<String, String> emailToUserId = new HashMap<>();
for (BulkImportUser user : users) {
for (LoginMethod lm : user.loginMethods) {
emailToUserId.put(lm.getSuperTokenOrExternalUserId(), lm.email);
}
}
Map<String, String> emailToUserId = collectVerifiedEmailAddressesByUserIds(users);
try {
verifyCollectedEmailAddressesForUsers(appIdentifier, storage, emailToUserId);
} catch (StorageQueryException | StorageTransactionLogicException e) {
if (e.getCause() instanceof BulkImportBatchInsertException) {
Map<String, Exception> errorsByPosition =
((BulkImportBatchInsertException) e.getCause()).exceptionByUserId;
for (String userid : errorsByPosition.keySet()) {
Exception exception = errorsByPosition.get(userid);
if (exception instanceof DuplicateEmailException) {
String message =
"E043: Email " + errorsByPosition.get(userid) + " is already verified for the user";
errorsByPosition.put(userid, new Exception(message));
} else if (exception instanceof NullPointerException) {
String message = "E044: null email address was found for the userId " + userid +
" while verifying the email";
errorsByPosition.put(userid, new Exception(message));
}
}
throw new StorageTransactionLogicException(
new BulkImportBatchInsertException("translated", errorsByPosition));
}
throw new StorageTransactionLogicException(e);
}
}
private static void verifyCollectedEmailAddressesForUsers(AppIdentifier appIdentifier, Storage storage,
Map<String, String> emailToUserId)
throws StorageQueryException, StorageTransactionLogicException {
if(!emailToUserId.isEmpty()) {
EmailVerificationSQLStorage emailVerificationSQLStorage = StorageUtils
.getEmailVerificationStorage(storage);
emailVerificationSQLStorage.startTransaction(con -> {
emailVerificationSQLStorage
.updateMultipleIsEmailVerified_Transaction(appIdentifier, con,
emailToUserId, true);
emailToUserId, true); //only the verified email addresses are expected to be in the map
emailVerificationSQLStorage.commitTransaction(con);
return null;
});
} catch (StorageQueryException e) {
throw new StorageTransactionLogicException(e);
}
}
@NotNull
private static Map<String, String> collectVerifiedEmailAddressesByUserIds(List<BulkImportUser> users) {
Map<String, String> emailToUserId = new LinkedHashMap<>();
for (BulkImportUser user : users) {
for (LoginMethod lm : user.loginMethods) {
//we skip passwordless` 'null' email addresses
if (lm.isVerified && !(lm.recipeId.equals("passwordless") && lm.email == null)) {
//collect the verified email addresses for the userId
emailToUserId.put(lm.getSuperTokenOrExternalUserId(), lm.email);
}
}
}
return emailToUserId;
}
public static void createMultipleTotpDevices(Main main, AppIdentifier appIdentifier,
Storage storage, List<BulkImportUser> users)
throws StorageTransactionLogicException {
@ -733,7 +768,7 @@ public class BulkImport {
for (BulkImportUser user : users) {
if (user.totpDevices != null) {
for(TotpDevice device : user.totpDevices){
TOTPDevice totpDevice = new TOTPDevice(getPrimaryLoginMethod(user).getSuperTokenOrExternalUserId(),
TOTPDevice totpDevice = new TOTPDevice(BulkImportUserUtils.getPrimaryLoginMethod(user).getSuperTokenOrExternalUserId(),
device.deviceName, device.secretKey, device.period, device.skew, true,
System.currentTimeMillis());
devices.add(totpDevice);
@ -751,21 +786,6 @@ public class BulkImport {
}
}
// Returns the primary loginMethod of the user. If no loginMethod is marked as
// primary, then the oldest loginMethod is returned.
public static BulkImportUser.LoginMethod getPrimaryLoginMethod(BulkImportUser user) {
BulkImportUser.LoginMethod oldestLM = user.loginMethods.get(0);
for (BulkImportUser.LoginMethod lm : user.loginMethods) {
if (lm.isPrimary) {
return lm;
}
if (lm.timeJoinedInMSSinceEpoch < oldestLM.timeJoinedInMSSinceEpoch) {
oldestLM = lm;
}
}
return oldestLM;
}
private static synchronized Storage getBulkImportProxyStorage(Main main, TenantIdentifier tenantIdentifier)
throws InvalidConfigException, IOException, TenantOrAppNotFoundException, DbInitException {

View File

@ -16,16 +16,9 @@
package io.supertokens.bulkimport;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.supertokens.Main;
import io.supertokens.bulkimport.exceptions.InvalidBulkImportDataException;
import io.supertokens.config.CoreConfig;
@ -37,16 +30,18 @@ import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.bulkimport.BulkImportUser;
import io.supertokens.pluginInterface.bulkimport.BulkImportUser.LoginMethod;
import io.supertokens.pluginInterface.bulkimport.BulkImportUser.UserRole;
import io.supertokens.pluginInterface.bulkimport.BulkImportUser.TotpDevice;
import io.supertokens.pluginInterface.bulkimport.BulkImportUser.UserRole;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantConfig;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.utils.Utils;
import io.supertokens.utils.JsonValidatorUtils.ValueType;
import io.supertokens.utils.Utils;
import java.util.*;
import static io.supertokens.utils.JsonValidatorUtils.parseAndValidateFieldType;
import static io.supertokens.utils.JsonValidatorUtils.validateJsonFieldType;
@ -60,8 +55,7 @@ public class BulkImportUserUtils {
this.allExternalUserIds = new HashSet<>();
}
public BulkImportUser createBulkImportUserFromJSON(Main main, AppIdentifier appIdentifier, JsonObject userData,
String id)
public BulkImportUser createBulkImportUserFromJSON(Main main, AppIdentifier appIdentifier, JsonObject userData, IDMode idMode)
throws InvalidBulkImportDataException, StorageQueryException, TenantOrAppNotFoundException {
List<String> errors = new ArrayList<>();
@ -72,7 +66,7 @@ public class BulkImportUserUtils {
JsonObject.class, errors, ".");
List<UserRole> userRoles = getParsedUserRoles(main, appIdentifier, userData, errors);
List<TotpDevice> totpDevices = getParsedTotpDevices(main, appIdentifier, userData, errors);
List<LoginMethod> loginMethods = getParsedLoginMethods(main, appIdentifier, userData, errors);
List<LoginMethod> loginMethods = getParsedLoginMethods(main, appIdentifier, userData, errors, idMode);
externalUserId = validateAndNormaliseExternalUserId(externalUserId, errors);
@ -81,6 +75,7 @@ public class BulkImportUserUtils {
if (!errors.isEmpty()) {
throw new InvalidBulkImportDataException(errors);
}
String id = getPrimaryLoginMethod(loginMethods).superTokensUserId;
return new BulkImportUser(id, externalUserId, userMetadata, userRoles, totpDevices, loginMethods);
}
@ -155,7 +150,7 @@ public class BulkImportUserUtils {
}
private List<LoginMethod> getParsedLoginMethods(Main main, AppIdentifier appIdentifier, JsonObject userData,
List<String> errors)
List<String> errors, IDMode idMode)
throws StorageQueryException, TenantOrAppNotFoundException {
JsonArray jsonLoginMethods = parseAndValidateFieldType(userData, "loginMethods", ValueType.ARRAY_OF_OBJECT,
true, JsonArray.class, errors, ".");
@ -193,6 +188,7 @@ public class BulkImportUserUtils {
Long timeJoined = parseAndValidateFieldType(jsonLoginMethodObj, "timeJoinedInMSSinceEpoch", ValueType.LONG,
false, Long.class, errors, " for a loginMethod");
recipeId = validateAndNormaliseRecipeId(recipeId, errors);
List<String> normalisedTenantIds = validateAndNormaliseTenantIds(main, appIdentifier, tenantIds, errors,
" for " + recipeId + " recipe.");
@ -201,6 +197,12 @@ public class BulkImportUserUtils {
long timeJoinedInMSSinceEpoch = validateAndNormaliseTimeJoined(timeJoined, errors);
String supertokensUserId = switch (idMode) {
case READ_STORED -> parseAndValidateFieldType(jsonLoginMethodObj, "superTokensUserId", ValueType.STRING,
true, String.class, errors, " for a loginMethod");
case GENERATE -> Utils.getUUID();
};
if ("emailpassword".equals(recipeId)) {
String email = parseAndValidateFieldType(jsonLoginMethodObj, "email", ValueType.STRING, true,
String.class, errors, " for an emailpassword recipe.");
@ -224,7 +226,8 @@ public class BulkImportUserUtils {
passwordHash, errors);
loginMethods.add(new LoginMethod(normalisedTenantIds, recipeId, isVerified, isPrimary,
timeJoinedInMSSinceEpoch, email, passwordHash, hashingAlgorithm, null, null, null, null));
timeJoinedInMSSinceEpoch, email, passwordHash, hashingAlgorithm, plainTextPassword,
null, null, null, supertokensUserId));
} else if ("thirdparty".equals(recipeId)) {
String email = parseAndValidateFieldType(jsonLoginMethodObj, "email", ValueType.STRING, true,
String.class, errors, " for a thirdparty recipe.");
@ -238,7 +241,8 @@ public class BulkImportUserUtils {
thirdPartyUserId = validateAndNormaliseThirdPartyUserId(thirdPartyUserId, errors);
loginMethods.add(new LoginMethod(normalisedTenantIds, recipeId, isVerified, isPrimary,
timeJoinedInMSSinceEpoch, email, null, null, null, thirdPartyId, thirdPartyUserId, null));
timeJoinedInMSSinceEpoch, email, null, null, null,
thirdPartyId, thirdPartyUserId, null, supertokensUserId));
} else if ("passwordless".equals(recipeId)) {
String email = parseAndValidateFieldType(jsonLoginMethodObj, "email", ValueType.STRING, false,
String.class, errors, " for a passwordless recipe.");
@ -253,7 +257,8 @@ public class BulkImportUserUtils {
}
loginMethods.add(new LoginMethod(normalisedTenantIds, recipeId, isVerified, isPrimary,
timeJoinedInMSSinceEpoch, email, null, null, null, null, null, phoneNumber));
timeJoinedInMSSinceEpoch, email, null, null, null,
null, null, phoneNumber, supertokensUserId));
}
}
return loginMethods;
@ -576,4 +581,74 @@ public class BulkImportUserUtils {
}
}
}
public static BulkImportUser.LoginMethod getPrimaryLoginMethod(BulkImportUser user) {
return getPrimaryLoginMethod(user.loginMethods);
}
// Returns the primary loginMethod of the user. If no loginMethod is marked as
// primary, then the oldest loginMethod is returned.
public static BulkImportUser.LoginMethod getPrimaryLoginMethod(List<LoginMethod> loginMethods) {
BulkImportUser.LoginMethod oldestLM = loginMethods.get(0);
for (BulkImportUser.LoginMethod lm : loginMethods) {
if (lm.isPrimary) {
return lm;
}
if (lm.timeJoinedInMSSinceEpoch < oldestLM.timeJoinedInMSSinceEpoch) {
oldestLM = lm;
}
}
return oldestLM;
}
public enum IDMode {
GENERATE,
READ_STORED;
}
// Returns a map of recipe user ids -> primary user ids
public static Map<String, String> collectRecipeIdsToPrimaryIds(List<BulkImportUser> users) {
Map<String, String> recipeUserIdByPrimaryUserId = new HashMap<>();
if(users == null){
return recipeUserIdByPrimaryUserId;
}
for(BulkImportUser user: users){
LoginMethod primaryLM = BulkImportUserUtils.getPrimaryLoginMethod(user);
for (LoginMethod lm : user.loginMethods) {
if (lm.getSuperTokenOrExternalUserId().equals(primaryLM.getSuperTokenOrExternalUserId())) {
continue;
}
recipeUserIdByPrimaryUserId.put(lm.getSuperTokenOrExternalUserId(),
primaryLM.getSuperTokenOrExternalUserId());
}
}
return recipeUserIdByPrimaryUserId;
}
public static LoginMethod findLoginMethodByRecipeUserId(List<BulkImportUser> users, String recipeUserId) {
if(users == null || users.isEmpty() || recipeUserId == null){
return null;
}
for(BulkImportUser user: users) {
for (LoginMethod loginMethod : user.loginMethods) {
if (recipeUserId.equals(loginMethod.superTokensUserId)) {
return loginMethod;
}
}
}
return null;
}
public static BulkImportUser findUserByPrimaryId(List<BulkImportUser> users, String primaryUserId) {
if(users == null || users.isEmpty() || primaryUserId == null){
return null;
}
for(BulkImportUser user: users) {
if(primaryUserId.equals(user.primaryUserId)){
return user;
}
}
return null;
}
}

View File

@ -52,6 +52,8 @@ public class Config extends ResourceDistributor.SingletonResource {
final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
Object configObj = mapper.readValue(new File(configFilePath), Object.class);
JsonObject jsonConfig = new GsonBuilder().serializeNulls().create().toJsonTree(configObj).getAsJsonObject();
CoreConfig.updateConfigJsonFromEnv(jsonConfig);
StorageLayer.updateConfigJsonFromEnv(main, jsonConfig);
CoreConfig config = ConfigMapper.mapConfig(jsonConfig, CoreConfig.class);
config.normalizeAndValidate(main, true);
this.core = config;
@ -91,12 +93,20 @@ public class Config extends ResourceDistributor.SingletonResource {
// omit them from the output json.
ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
Object obj = yamlReader.readValue(new File(getConfigFilePath(main)), Object.class);
return new GsonBuilder().serializeNulls().create().toJsonTree(obj).getAsJsonObject();
JsonObject configJson = new GsonBuilder().serializeNulls().create().toJsonTree(obj).getAsJsonObject();
CoreConfig.updateConfigJsonFromEnv(configJson);
StorageLayer.updateConfigJsonFromEnv(main, configJson);
return configJson;
}
private static String getConfigFilePath(Main main) {
String configFile = "config.yaml";
if (Main.isTesting) {
String workerId = System.getProperty("org.gradle.test.worker", "");
configFile = "config" + workerId + ".yaml";
}
return CLIOptions.get(main).getConfigFilePath() == null
? CLIOptions.get(main).getInstallationPath() + "config.yaml"
? CLIOptions.get(main).getInstallationPath() + configFile
: CLIOptions.get(main).getConfigFilePath();
}
@ -305,7 +315,7 @@ public class Config extends ResourceDistributor.SingletonResource {
@TestOnly
public static CoreConfig getConfig(Main main) {
try {
return getConfig(new TenantIdentifier(null, null, null), main);
return getConfig(ResourceDistributor.getAppForTesting(), main);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}

View File

@ -67,7 +67,8 @@ public class CoreConfig {
"oauth_provider_public_service_url",
"oauth_provider_admin_service_url",
"oauth_provider_consent_login_base_url",
"oauth_provider_url_configured_in_oauth_provider"
"oauth_provider_url_configured_in_oauth_provider",
"saml_legacy_acs_url"
};
@IgnoreForAnnotationCheck
@ -75,11 +76,13 @@ public class CoreConfig {
@ConfigDescription("The version of the core config.")
private int core_config_version = -1;
@EnvName("ACCESS_TOKEN_VALIDITY")
@NotConflictingInApp
@JsonProperty
@ConfigDescription("Time in seconds for how long an access token is valid for. [Default: 3600 (1 hour)]")
private long access_token_validity = 3600; // in seconds
@EnvName("ACCESS_TOKEN_BLACKLISTING")
@NotConflictingInApp
@JsonProperty
@ConfigDescription(
@ -88,17 +91,20 @@ public class CoreConfig {
"call that requires authentication. (Default: false)")
private boolean access_token_blacklisting = false;
@EnvName("REFRESH_TOKEN_VALIDITY")
@NotConflictingInApp
@JsonProperty
@ConfigDescription("Time in mins for how long a refresh token is valid for. [Default: 60 * 2400 (100 days)]")
private double refresh_token_validity = 60 * 2400; // in mins
@EnvName("PASSWORD_RESET_TOKEN_LIFETIME")
@IgnoreForAnnotationCheck
@JsonProperty
@ConfigDescription(
"Time in milliseconds for how long a password reset token / link is valid for. [Default: 3600000 (1 hour)]")
private long password_reset_token_lifetime = 3600000; // in MS
@EnvName("EMAIL_VERIFICATION_TOKEN_LIFETIME")
@IgnoreForAnnotationCheck
@JsonProperty
@ConfigDescription(
@ -106,23 +112,27 @@ public class CoreConfig {
" 1000 (1 day)]")
private long email_verification_token_lifetime = 24 * 3600 * 1000; // in MS
@EnvName("PASSWORDLESS_MAX_CODE_INPUT_ATTEMPTS")
@IgnoreForAnnotationCheck
@JsonProperty
@ConfigDescription(
"The maximum number of code input attempts per login before the user needs to restart. (Default: 5)")
private int passwordless_max_code_input_attempts = 5;
@EnvName("PASSWORDLESS_CODE_LIFETIME")
@IgnoreForAnnotationCheck
@JsonProperty
@ConfigDescription(
"Time in milliseconds for how long a passwordless code is valid for. [Default: 900000 (15 mins)]")
private long passwordless_code_lifetime = 900000; // in MS
@EnvName("TOTP_MAX_ATTEMPTS")
@IgnoreForAnnotationCheck
@JsonProperty
@ConfigDescription("The maximum number of invalid TOTP attempts that will trigger rate limiting. (Default: 5)")
private int totp_max_attempts = 5;
@EnvName("TOTP_RATE_LIMIT_COOLDOWN_SEC")
@IgnoreForAnnotationCheck
@JsonProperty
@ConfigDescription(
@ -133,6 +143,7 @@ public class CoreConfig {
@IgnoreForAnnotationCheck
private final String logDefault = "asdkfahbdfk3kjHS";
@EnvName("INFO_LOG_PATH")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
@ -141,6 +152,7 @@ public class CoreConfig {
"directory/logs/info.log)")
private String info_log_path = logDefault;
@EnvName("ERROR_LOG_PATH")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
@ -149,6 +161,7 @@ public class CoreConfig {
"directory/logs/error.log)")
private String error_log_path = logDefault;
@EnvName("ACCESS_TOKEN_SIGNING_KEY_DYNAMIC")
@NotConflictingInApp
@JsonProperty
@ConfigDescription(
@ -156,17 +169,20 @@ public class CoreConfig {
" be signed using a static signing key. (Default: true)")
private boolean access_token_signing_key_dynamic = true;
@EnvName("ACCESS_TOKEN_DYNAMIC_SIGNING_KEY_UPDATE_INTERVAL")
@NotConflictingInApp
@JsonProperty("access_token_dynamic_signing_key_update_interval")
@JsonAlias({"access_token_dynamic_signing_key_update_interval", "access_token_signing_key_update_interval"})
@ConfigDescription("Time in hours for how frequently the dynamic signing key will change. [Default: 168 (1 week)]")
private double access_token_dynamic_signing_key_update_interval = 168; // in hours
@EnvName("SUPERTOKENS_PORT")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription("The port at which SuperTokens service runs. (Default: 3567)")
private int port = 3567;
@EnvName("SUPERTOKENS_HOST")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
@ -174,11 +190,13 @@ public class CoreConfig {
" address associated with your machine. (Default: localhost)")
private String host = "localhost";
@EnvName("MAX_SERVER_POOL_SIZE")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription("Sets the max thread pool size for incoming http server requests. (Default: 10)")
private int max_server_pool_size = 10;
@EnvName("API_KEYS")
@NotConflictingInApp
@JsonProperty
@HideFromDashboard
@ -188,6 +206,7 @@ public class CoreConfig {
"length of 20 chars. (Default: null)")
private String api_keys = null;
@EnvName("DISABLE_TELEMETRY")
@NotConflictingInApp
@JsonProperty
@ConfigDescription(
@ -195,27 +214,32 @@ public class CoreConfig {
"(Default: false)")
private boolean disable_telemetry = false;
@EnvName("PASSWORD_HASHING_ALG")
@NotConflictingInApp
@JsonProperty
@ConfigDescription("The password hashing algorithm to use. Values are \"ARGON2\" | \"BCRYPT\". (Default: BCRYPT)")
@EnumProperty({"ARGON2", "BCRYPT"})
private String password_hashing_alg = "BCRYPT";
@EnvName("ARGON2_ITERATIONS")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription("Number of iterations for argon2 password hashing. (Default: 1)")
private int argon2_iterations = 1;
@EnvName("ARGON2_MEMORY_KB")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription("Amount of memory in kb for argon2 password hashing. [Default: 87795 (85 mb)]")
private int argon2_memory_kb = 87795; // 85 mb
@EnvName("ARGON2_PARALLELISM")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription("Amount of parallelism for argon2 password hashing. (Default: 2)")
private int argon2_parallelism = 2;
@EnvName("ARGON2_HASHING_POOL_SIZE")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
@ -223,6 +247,7 @@ public class CoreConfig {
"(Default: 1)")
private int argon2_hashing_pool_size = 1;
@EnvName("FIREBASE_PASSWORD_HASHING_POOL_SIZE")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
@ -230,6 +255,7 @@ public class CoreConfig {
"(Default: 1)")
private int firebase_password_hashing_pool_size = 1;
@EnvName("BCRYPT_LOG_ROUNDS")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription("Number of rounds to set for bcrypt password hashing. (Default: 11)")
@ -245,13 +271,16 @@ public class CoreConfig {
// # webserver_https_enabled:
@ConfigYamlOnly
@JsonProperty
@IgnoreForAnnotationCheck
private boolean webserver_https_enabled = false;
@EnvName("BASE_PATH")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription("Used to prepend a base path to all APIs when querying the core.")
private String base_path = "";
@EnvName("LOG_LEVEL")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
@ -260,11 +289,13 @@ public class CoreConfig {
@EnumProperty({"DEBUG", "INFO", "WARN", "ERROR", "NONE"})
private String log_level = "INFO";
@EnvName("FIREBASE_PASSWORD_HASHING_SIGNER_KEY")
@NotConflictingInApp
@JsonProperty
@ConfigDescription("The signer key used for firebase scrypt password hashing. (Default: null)")
private String firebase_password_hashing_signer_key = null;
@EnvName("IP_ALLOW_REGEX")
@IgnoreForAnnotationCheck
@JsonProperty
@ConfigDescription(
@ -272,6 +303,7 @@ public class CoreConfig {
"127\\.\\d+\\.\\d+\\.\\d+|::1|0:0:0:0:0:0:0:1 to allow only localhost to query the core")
private String ip_allow_regex = null;
@EnvName("IP_DENY_REGEX")
@IgnoreForAnnotationCheck
@JsonProperty
@ConfigDescription(
@ -279,6 +311,7 @@ public class CoreConfig {
" address.")
private String ip_deny_regex = null;
@EnvName("OAUTH_PROVIDER_PUBLIC_SERVICE_URL")
@NotConflictingInApp
@JsonProperty
@HideFromDashboard
@ -286,6 +319,7 @@ public class CoreConfig {
"If specified, the core uses this URL to connect to the OAuth provider public service.")
private String oauth_provider_public_service_url = null;
@EnvName("OAUTH_PROVIDER_ADMIN_SERVICE_URL")
@NotConflictingInApp
@JsonProperty
@HideFromDashboard
@ -293,6 +327,7 @@ public class CoreConfig {
"If specified, the core uses this URL to connect to the OAuth provider admin service.")
private String oauth_provider_admin_service_url = null;
@EnvName("OAUTH_PROVIDER_CONSENT_LOGIN_BASE_URL")
@NotConflictingInApp
@JsonProperty
@HideFromDashboard
@ -300,6 +335,7 @@ public class CoreConfig {
"If specified, the core uses this URL to replace the default consent and login URLs to {apiDomain}.")
private String oauth_provider_consent_login_base_url = null;
@EnvName("OAUTH_PROVIDER_URL_CONFIGURED_IN_OAUTH_PROVIDER")
@NotConflictingInApp
@JsonProperty
@HideFromDashboard
@ -307,12 +343,14 @@ public class CoreConfig {
"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;
@EnvName("OAUTH_CLIENT_SECRET_ENCRYPTION_KEY")
@ConfigYamlOnly
@JsonProperty
@HideFromDashboard
@ConfigDescription("The encryption key used for saving OAuth client secret on the database.")
private String oauth_client_secret_encryption_key = null;
@EnvName("SUPERTOKENS_SAAS_SECRET")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
@ -322,6 +360,7 @@ public class CoreConfig {
"regular api_keys config.")
private String supertokens_saas_secret = null;
@EnvName("SUPERTOKENS_MAX_CDI_VERSION")
@NotConflictingInApp
@JsonProperty
@HideFromDashboard
@ -331,6 +370,7 @@ public class CoreConfig {
"null)")
private String supertokens_max_cdi_version = null;
@EnvName("SUPERTOKENS_SAAS_LOAD_ONLY_CUD")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
@ -338,18 +378,73 @@ public class CoreConfig {
"the database and block all other CUDs from being used from this instance.")
private String supertokens_saas_load_only_cud = null;
@EnvName("SAML_LEGACY_ACS_URL")
@NotConflictingInApp
@JsonProperty
@ConfigDescription("If specified, uses this URL as ACS URL for handling legacy SAML clients")
@HideFromDashboard
private String saml_legacy_acs_url = null;
@EnvName("SAML_SP_ENTITY_ID")
@JsonProperty
@IgnoreForAnnotationCheck
@ConfigDescription("Service provider's entity ID")
private String saml_sp_entity_id = null;
@EnvName("SAML_CLAIMS_VALIDITY")
@JsonProperty
@IgnoreForAnnotationCheck
@ConfigDescription("Duration for which SAML claims will be valid before it is consumed")
private long saml_claims_validity = 300000;
@EnvName("SAML_RELAY_STATE_VALIDITY")
@JsonProperty
@IgnoreForAnnotationCheck
@ConfigDescription("Duration for which SAML relay state will be valid before it is consumed")
private long saml_relay_state_validity = 300000;
@IgnoreForAnnotationCheck
private Set<LOG_LEVEL> allowedLogLevels = null;
@IgnoreForAnnotationCheck
private boolean isNormalizedAndValid = false;
@EnvName("BULK_MIGRATION_PARALLELISM")
@NotConflictingInApp
@JsonProperty
@ConfigDescription("If specified, the supertokens core will use the specified number of threads to complete the " +
"migration of users. (Default: number of available processor cores).")
private int bulk_migration_parallelism = Runtime.getRuntime().availableProcessors();
@EnvName("BULK_MIGRATION_BATCH_SIZE")
@NotConflictingInApp
@JsonProperty
@ConfigDescription("If specified, the supertokens core will load the specified number of users for migrating in " +
"one single batch. (Default: 8000)")
private int bulk_migration_batch_size = 8000;
@EnvName("WEBAUTHN_RECOVER_ACCOUNT_TOKEN_LIFETIME")
@NotConflictingInApp
@JsonProperty
@ConfigDescription("Time in milliseconds for how long a webauthn account recovery token is valid for. [Default: 3600000 (1 hour)]")
private long webauthn_recover_account_token_lifetime = 3600000; // in MS;
@EnvName("OTEL_COLLECTOR_CONNECTION_URI")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
"The URL of the OpenTelemetry collector to which the core will send telemetry data. " +
"This should be in the format http://<host>:<port> or https://<host>:<port>. (Default: " +
"null)")
private String otel_collector_connection_uri = null;
@EnvName("DEADLOCK_LOGGER_ENABLE")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
"Enables or disables the deadlock logger. (Default: false)")
private boolean deadlock_logger_enable = false;
@IgnoreForAnnotationCheck
private static boolean disableOAuthValidationForTest = false;
@ -418,6 +513,10 @@ public class CoreConfig {
return ip_deny_regex;
}
public String getLogLevel() {
return log_level;
}
public Set<LOG_LEVEL> getLogLevels(Main main) {
if (allowedLogLevels != null) {
return allowedLogLevels;
@ -589,12 +688,84 @@ public class CoreConfig {
return bulk_migration_parallelism;
}
public long getWebauthnRecoverAccountTokenLifetime() {
return webauthn_recover_account_token_lifetime;
}
public int getBulkMigrationBatchSize() {
return bulk_migration_batch_size;
}
public String getOtelCollectorConnectionURI() {
return otel_collector_connection_uri;
}
public boolean isDeadlockLoggerEnabled() {
return deadlock_logger_enable;
}
public String getSAMLLegacyACSURL() {
return saml_legacy_acs_url;
}
public String getSAMLSPEntityID() {
return saml_sp_entity_id;
}
public long getSAMLClaimsValidity() {
return saml_claims_validity;
}
public long getSAMLRelayStateValidity() {
return saml_relay_state_validity;
}
private String getConfigFileLocation(Main main) {
return new File(CLIOptions.get(main).getConfigFilePath() == null
? CLIOptions.get(main).getInstallationPath() + "config.yaml"
: CLIOptions.get(main).getConfigFilePath()).getAbsolutePath();
}
public static void updateConfigJsonFromEnv(JsonObject configJson) {
Map<String, String> env = System.getenv();
for (Field field : CoreConfig.class.getDeclaredFields()) {
if (field.isAnnotationPresent(EnvName.class)) {
String envName = field.getAnnotation(EnvName.class).value();
String stringValue = env.get(envName);
if (stringValue == null || stringValue.isEmpty()) {
continue;
}
if (stringValue.startsWith("\"") && stringValue.endsWith("\"")) {
stringValue = stringValue.substring(1, stringValue.length() - 1);
stringValue = stringValue
.replace("\\n", "\n")
.replace("\\t", "\t")
.replace("\\r", "\r")
.replace("\\\"", "\"")
.replace("\\'", "'")
.replace("\\\\", "\\");
}
if (field.getType().equals(String.class)) {
configJson.addProperty(field.getName(), stringValue);
} else if (field.getType().equals(int.class)) {
configJson.addProperty(field.getName(), Integer.parseInt(stringValue));
} else if (field.getType().equals(long.class)) {
configJson.addProperty(field.getName(), Long.parseLong(stringValue));
} else if (field.getType().equals(boolean.class)) {
configJson.addProperty(field.getName(), Boolean.parseBoolean(stringValue));
} else if (field.getType().equals(float.class)) {
configJson.addProperty(field.getName(), Float.parseFloat(stringValue));
} else if (field.getType().equals(double.class)) {
configJson.addProperty(field.getName(), Double.parseDouble(stringValue));
}
}
}
}
void normalizeAndValidate(Main main, boolean includeConfigFilePath) throws InvalidConfigException {
if (isNormalizedAndValid) {
return;
@ -786,6 +957,14 @@ public class CoreConfig {
throw new InvalidConfigException("Provided bulk_migration_parallelism must be >= 1");
}
if (bulk_migration_batch_size < 1) {
throw new InvalidConfigException("Provided bulk_migration_batch_size must be >= 1");
}
if (webauthn_recover_account_token_lifetime <= 0) {
throw new InvalidConfigException("Provided webauthn_recover_account_token_lifetime must be > 0");
}
for (String fieldId : CoreConfig.getValidFields()) {
try {
Field field = CoreConfig.class.getDeclaredField(fieldId);
@ -809,6 +988,10 @@ public class CoreConfig {
}
// Normalize
if (saml_sp_entity_id == null) {
saml_sp_entity_id = "https://saml.supertokens.com";
}
if (ip_allow_regex != null) {
ip_allow_regex = ip_allow_regex.trim();
if (ip_allow_regex.equals("")) {
@ -946,6 +1129,24 @@ public class CoreConfig {
}
}
if (Main.isTesting) {
if (oauth_provider_public_service_url == null) {
oauth_provider_public_service_url = "http://localhost:" + System.getProperty("ST_OAUTH_PROVIDER_SERVICE_PORT");
}
if (oauth_provider_admin_service_url == null) {
oauth_provider_admin_service_url = "http://localhost:" + System.getProperty("ST_OAUTH_PROVIDER_ADMIN_PORT");
}
if (oauth_provider_url_configured_in_oauth_provider == null) {
oauth_provider_url_configured_in_oauth_provider = "http://localhost:4444";
}
if (oauth_client_secret_encryption_key == null) {
oauth_client_secret_encryption_key = "clientsecretencryptionkey";
}
if (oauth_provider_consent_login_base_url == null) {
oauth_provider_consent_login_base_url = "http://localhost:3001/auth";
}
}
isNormalizedAndValid = true;
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2025, 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.config.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
// Make annotation accessible at runtime so that config can be read from env
@Target(ElementType.FIELD) // Annotation can only be applied to fields
public @interface EnvName {
String value(); // String value that provides a env var name for the field
}

View File

@ -22,6 +22,7 @@ import io.supertokens.bulkimport.BulkImportUserUtils;
import io.supertokens.config.Config;
import io.supertokens.cronjobs.CronTask;
import io.supertokens.cronjobs.CronTaskTest;
import io.supertokens.output.Logging;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.StorageUtils;
import io.supertokens.pluginInterface.bulkimport.BulkImportStorage;
@ -46,7 +47,7 @@ import java.util.stream.Stream;
public class ProcessBulkImportUsers extends CronTask {
public static final String RESOURCE_KEY = "io.supertokens.ee.cronjobs.ProcessBulkImportUsers";
public static final String RESOURCE_KEY = "io.supertokens.cronjobs.ProcessBulkImportUsers";
private ExecutorService executorService;
@ -72,32 +73,49 @@ public class ProcessBulkImportUsers extends CronTask {
.getStorage(app.getAsPublicTenantIdentifier(), main);
//split the loaded users list into smaller chunks
int NUMBER_OF_BATCHES = Config.getConfig(app.getAsPublicTenantIdentifier(), main)
int numberOfBatchChunks = Config.getConfig(app.getAsPublicTenantIdentifier(), main)
.getBulkMigrationParallelism();
executorService = Executors.newFixedThreadPool(NUMBER_OF_BATCHES);
int bulkMigrationBatchSize = Config.getConfig(app.getAsPublicTenantIdentifier(), main)
.getBulkMigrationBatchSize();
Logging.debug(main, app.getAsPublicTenantIdentifier(), "CronTask starts. Instance: " + this);
Logging.debug(main, app.getAsPublicTenantIdentifier(), "CronTask starts. Processing bulk import users with " + bulkMigrationBatchSize
+ " batch size, one batch split into " + numberOfBatchChunks + " chunks");
executorService = Executors.newFixedThreadPool(numberOfBatchChunks);
String[] allUserRoles = StorageUtils.getUserRolesStorage(bulkImportSQLStorage).getRoles(app);
BulkImportUserUtils bulkImportUserUtils = new BulkImportUserUtils(allUserRoles);
long newUsers = bulkImportSQLStorage.getBulkImportUsersCount(app, BulkImportStorage.BULK_IMPORT_USER_STATUS.NEW);
long processingUsers = bulkImportSQLStorage.getBulkImportUsersCount(app, BulkImportStorage.BULK_IMPORT_USER_STATUS.PROCESSING);
long failedUsers = 0;
//taking a "snapshot" here and processing in this round as many users as there are uploaded now. After this the processing will go on
//with another app and gets back here when all the apps had a chance.
long usersProcessed = 0;
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Found " + (newUsers + processingUsers) + " waiting for processing"
+ " (" + newUsers + " new, " + processingUsers + " processing)");;
while(usersProcessed < (newUsers + processingUsers)) {
List<BulkImportUser> users = bulkImportSQLStorage.getBulkImportUsersAndChangeStatusToProcessing(app,
BulkImport.PROCESS_USERS_BATCH_SIZE);
bulkMigrationBatchSize);
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Loaded " + users.size() + " users to process");
if (users == null || users.isEmpty()) {
// "No more users to process!"
break;
}
List<List<BulkImportUser>> loadedUsersChunks = makeChunksOf(users, NUMBER_OF_BATCHES);
List<List<BulkImportUser>> loadedUsersChunks = makeChunksOf(users, numberOfBatchChunks);
for (List<BulkImportUser> chunk : loadedUsersChunks) {
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Chunk size: " + chunk.size());
}
try {
List<Future<?>> tasks = new ArrayList<>();
for (int i = 0; i < NUMBER_OF_BATCHES && i < loadedUsersChunks.size(); i++) {
for (int i = 0; i < numberOfBatchChunks && i < loadedUsersChunks.size(); i++) {
tasks.add(
executorService.submit(new ProcessBulkUsersImportWorker(main, app, loadedUsersChunks.get(i),
bulkImportSQLStorage, bulkImportUserUtils)));
@ -105,13 +123,30 @@ public class ProcessBulkImportUsers extends CronTask {
for (Future<?> task : tasks) {
while (!task.isDone()) {
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Waiting for task " + task + " to finish");
Thread.sleep(1000);
}
Void result = (Void) task.get(); //to know if there were any errors while executing and for waiting in this thread for all the other threads to finish up
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Task " + task + " finished");
try {
Void result = (Void) task.get(); //to know if there were any errors while executing and for
// waiting in this thread for all the other threads to finish up
Logging.debug(main, app.getAsPublicTenantIdentifier(),
"Task " + task + " finished with result: " + result);
} catch (ExecutionException executionException) {
Logging.error(main, app.getAsPublicTenantIdentifier(),
"Error while processing bulk import users", true,
executionException);
throw new RuntimeException(executionException);
}
usersProcessed += loadedUsersChunks.get(tasks.indexOf(task)).size();
failedUsers = bulkImportSQLStorage.getBulkImportUsersCount(app, BulkImportStorage.BULK_IMPORT_USER_STATUS.FAILED);
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Chunk " + tasks.indexOf(task) + " finished processing, all chunks processed: "
+ usersProcessed + " users (" + failedUsers + " failed)");
}
} catch (ExecutionException | InterruptedException e) {
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Processing round finished");
} catch (InterruptedException e) {
Logging.error(main, app.getAsPublicTenantIdentifier(), "Error while processing bulk import users", true,
e);
throw new RuntimeException(e);
}
}

View File

@ -77,6 +77,8 @@ public class ProcessBulkUsersImportWorker implements Runnable {
DbInitException {
BulkImportUser user = null;
try {
Logging.debug(main, appIdentifier.getAsPublicTenantIdentifier(),
"Processing bulk import users: " + users.size());
final Storage[] allStoragesForApp = getAllProxyStoragesForApp(main, appIdentifier);
int userIndexPointer = 0;
List<BulkImportUser> validUsers = new ArrayList<>();
@ -92,7 +94,7 @@ public class ProcessBulkUsersImportWorker implements Runnable {
// Validate the user
try {
validUsers.add(bulkImportUserUtils.createBulkImportUserFromJSON(main, appIdentifier,
user.toJsonObject(), user.id));
user.toJsonObject(), BulkImportUserUtils.IDMode.READ_STORED));
} catch (InvalidBulkImportDataException exception) {
validationErrorsBeforeActualProcessing.put(user.id, new Exception(
String.valueOf(exception.errors)));
@ -107,12 +109,14 @@ public class ProcessBulkUsersImportWorker implements Runnable {
// Since all the tenants of a user must share the storage, we will just use the
// storage of the first tenantId of the first loginMethod
Map<SQLStorage, List<BulkImportUser>> partitionedUsers = partitionUsersByStorage(appIdentifier, validUsers);
for(SQLStorage bulkImportProxyStorage : partitionedUsers.keySet()) {
boolean shouldRetryImmediatley = true;
while (shouldRetryImmediatley) {
shouldRetryImmediatley = bulkImportProxyStorage.startTransaction(con -> {
try {
BulkImport.processUsersImportSteps(main, appIdentifier, bulkImportProxyStorage, partitionedUsers.get(bulkImportProxyStorage),
BulkImport.processUsersImportSteps(main, appIdentifier, bulkImportProxyStorage,
partitionedUsers.get(bulkImportProxyStorage),
allStoragesForApp);
bulkImportProxyStorage.commitTransactionForBulkImportProxyStorage();
@ -122,8 +126,18 @@ public class ProcessBulkUsersImportWorker implements Runnable {
toDelete[i] = validUsers.get(i).id;
}
baseTenantStorage.deleteBulkImportUsers(appIdentifier, toDelete);
} catch (StorageTransactionLogicException e) {
while (true){
try {
List<String> deletedIds = baseTenantStorage.deleteBulkImportUsers(appIdentifier,
toDelete);
break;
} catch (Exception e) {
// ignore and retry delete. The import transaction is already committed, the delete should happen no matter what
Logging.debug(main, app.getAsPublicTenantIdentifier(),
"Exception while deleting bulk import users: " + e.getMessage());
}
}
} catch (StorageTransactionLogicException | StorageQueryException e) {
// We need to rollback the transaction manually because we have overridden that in the proxy
// storage
bulkImportProxyStorage.rollbackTransactionForBulkImportProxyStorage();
@ -138,9 +152,15 @@ public class ProcessBulkUsersImportWorker implements Runnable {
}
}
} catch (StorageTransactionLogicException | InvalidConfigException e) {
Logging.error(main, app.getAsPublicTenantIdentifier(),
"Error while processing bulk import users: " + e.getMessage(), true, e);
throw new RuntimeException(e);
} catch (BulkImportBatchInsertException insertException) {
handleProcessUserExceptions(app, users, insertException, baseTenantStorage);
} catch (Exception e) {
Logging.error(main, app.getAsPublicTenantIdentifier(),
"Error while processing bulk import users: " + e.getMessage(), true, e);
throw e;
} finally {
closeAllProxyStorages(); //closing it here to reuse the existing connection with all the users
}
@ -163,30 +183,40 @@ public class ProcessBulkUsersImportWorker implements Runnable {
String[] errorMessage = { e.getMessage() };
Map<String, String> bulkImportUserIdToErrorMessage = new HashMap<>();
if (e instanceof StorageTransactionLogicException) {
StorageTransactionLogicException exception = (StorageTransactionLogicException) e;
// If the exception is due to a StorageQueryException, we want to retry the entry after sometime instead
// of marking it as FAILED. We will return early in that case.
if (exception.actualException instanceof StorageQueryException) {
Logging.error(main, null, "We got an StorageQueryException while processing a bulk import user entry. It will be retried again. Error Message: " + e.getMessage(), true);
return;
}
if(exception.actualException instanceof BulkImportBatchInsertException){
handleBulkImportException(usersBatch, (BulkImportBatchInsertException) exception.actualException, bulkImportUserIdToErrorMessage);
} else {
//fail the whole batch
errorMessage[0] = exception.actualException.getMessage();
for(BulkImportUser user : usersBatch){
bulkImportUserIdToErrorMessage.put(user.id, errorMessage[0]);
switch (e) {
case StorageTransactionLogicException exception -> {
// If the exception is due to a StorageQueryException, we want to retry the entry after sometime instead
// of marking it as FAILED. We will return early in that case.
if (exception.actualException instanceof StorageQueryException) {
Logging.error(main, null,
"We got an StorageQueryException while processing a bulk import user entry. It will be " +
"retried again. Error Message: " +
e.getMessage(), true);
return;
}
if (exception.actualException instanceof BulkImportBatchInsertException) {
handleBulkImportException(usersBatch, (BulkImportBatchInsertException) exception.actualException,
bulkImportUserIdToErrorMessage);
} else {
//fail the whole batch
errorMessage[0] = exception.actualException.getMessage();
for (BulkImportUser user : usersBatch) {
bulkImportUserIdToErrorMessage.put(user.id, errorMessage[0]);
}
}
}
} else if (e instanceof InvalidBulkImportDataException) {
errorMessage[0] = ((InvalidBulkImportDataException) e).errors.toString();
} else if (e instanceof InvalidConfigException) {
errorMessage[0] = e.getMessage();
} else if (e instanceof BulkImportBatchInsertException) {
handleBulkImportException(usersBatch, (BulkImportBatchInsertException) e, bulkImportUserIdToErrorMessage);
case InvalidBulkImportDataException invalidBulkImportDataException ->
errorMessage[0] = invalidBulkImportDataException.errors.toString();
case InvalidConfigException invalidConfigException -> errorMessage[0] = e.getMessage();
case BulkImportBatchInsertException bulkImportBatchInsertException ->
handleBulkImportException(usersBatch, bulkImportBatchInsertException,
bulkImportUserIdToErrorMessage);
default -> {
Logging.error(main, null,
"We got an error while processing a bulk import user entry. It will be " +
"retried again. Error Message: " +
e.getMessage(), true);
}
}
try {
@ -205,7 +235,7 @@ public class ProcessBulkUsersImportWorker implements Runnable {
Map<String, Exception> userIndexToError = exception.exceptionByUserId;
for(String userid : userIndexToError.keySet()){
Optional<BulkImportUser> userWithId = usersBatch.stream()
.filter(bulkImportUser -> bulkImportUser.id.equals(userid) || bulkImportUser.externalUserId.equals(userid)).findFirst();
.filter(bulkImportUser -> userid.equals(bulkImportUser.id) || userid.equals(bulkImportUser.externalUserId)).findFirst();
String id = null;
if(userWithId.isPresent()){
id = userWithId.get().id;
@ -284,7 +314,7 @@ public class ProcessBulkUsersImportWorker implements Runnable {
Map<SQLStorage, List<BulkImportUser>> result = new HashMap<>();
for(BulkImportUser user: users) {
TenantIdentifier firstTenantIdentifier = new TenantIdentifier(appIdentifier.getConnectionUriDomain(),
appIdentifier.getAppId(), user.loginMethods.get(0).tenantIds.get(0));
appIdentifier.getAppId(), user.loginMethods.getFirst().tenantIds.getFirst());
SQLStorage bulkImportProxyStorage = (SQLStorage) getBulkImportProxyStorage(firstTenantIdentifier);
if(!result.containsKey(bulkImportProxyStorage)){

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) 2025, 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.cronjobs.cleanupWebauthnExpiredData;
import io.supertokens.Main;
import io.supertokens.cronjobs.CronTask;
import io.supertokens.cronjobs.CronTaskTest;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.StorageUtils;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.webauthn.WebAuthNStorage;
import java.util.List;
public class CleanUpWebauthNExpiredDataCron extends CronTask {
public static final String RESOURCE_KEY = "io.supertokens.cronjobs.cleanupWebauthnExpiredData" +
".CleanUpWebauthnExpiredDataCron";
private CleanUpWebauthNExpiredDataCron(Main main, List<List<TenantIdentifier>> tenantsInfo) {
super("CleanUpWebauthnExpiredDataCron", main, tenantsInfo, true);
}
public static CleanUpWebauthNExpiredDataCron init(Main main, List<List<TenantIdentifier>> tenantsInfo) {
return (CleanUpWebauthNExpiredDataCron) main.getResourceDistributor()
.setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY,
new CleanUpWebauthNExpiredDataCron(main, tenantsInfo));
}
@Override
protected void doTaskPerStorage(Storage storage) throws Exception {
if (storage.getType() != STORAGE_TYPE.SQL) {
return;
}
WebAuthNStorage webAuthNStorage = StorageUtils.getWebAuthNStorage(storage);
webAuthNStorage.deleteExpiredAccountRecoveryTokens();
webAuthNStorage.deleteExpiredGeneratedOptions();
}
@Override
public int getIntervalTimeSeconds() {
if (Main.isTesting) {
Integer interval = CronTaskTest.getInstance(main).getIntervalInSeconds(RESOURCE_KEY);
if (interval != null) {
return interval;
}
}
// Every 24 hours.
return 24 * 3600;
}
@Override
public int getInitialWaitTimeSeconds() {
if (!Main.isTesting) {
return getIntervalTimeSeconds();
} else {
return 0;
}
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) 2025, 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.cronjobs.deadlocklogger;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.Arrays;
public class DeadlockLogger {
private static final DeadlockLogger INSTANCE = new DeadlockLogger();
private DeadlockLogger() {
}
public static DeadlockLogger getInstance() {
return INSTANCE;
}
public void start(){
Thread deadlockLoggerThread = new Thread(deadlockDetector, "DeadlockLoggerThread");
deadlockLoggerThread.setDaemon(true);
deadlockLoggerThread.start();
}
private final Runnable deadlockDetector = new Runnable() {
@Override
public void run() {
System.out.println("DeadlockLogger started!");
while (true) {
System.out.println("DeadlockLogger - checking");
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] threadIds = bean.findDeadlockedThreads(); // Returns null if no threads are deadlocked.
System.out.println("DeadlockLogger - DeadlockedThreads: " + Arrays.toString(threadIds));
if (threadIds != null) {
ThreadInfo[] infos = bean.getThreadInfo(threadIds);
boolean deadlockFound = false;
System.out.println("DEADLOCK found!");
for (ThreadInfo info : infos) {
System.out.println("ThreadName: " + info.getThreadName());
System.out.println("Thread ID: " + info.getThreadId());
System.out.println("LockName: " + info.getLockName());
System.out.println("LockOwnerName: " + info.getLockOwnerName());
System.out.println("LockedMonitors: " + Arrays.toString(info.getLockedMonitors()));
System.out.println("LockInfo: " + info.getLockInfo());
System.out.println("Stack: " + Arrays.toString(info.getStackTrace()));
System.out.println();
deadlockFound = true;
}
System.out.println("*******************************");
if(deadlockFound) {
System.out.println(" ==== ALL THREAD INFO ===");
ThreadInfo[] allThreads = bean.dumpAllThreads(true, true, 100);
for (ThreadInfo threadInfo : allThreads) {
System.out.println("THREAD: " + threadInfo.getThreadName());
System.out.println("StackTrace: " + Arrays.toString(threadInfo.getStackTrace()));
}
break;
}
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
}

View File

@ -0,0 +1,53 @@
package io.supertokens.cronjobs.deleteExpiredSAMLData;
import java.util.List;
import io.supertokens.Main;
import io.supertokens.cronjobs.CronTask;
import io.supertokens.cronjobs.CronTaskTest;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.StorageUtils;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.saml.SAMLStorage;
public class DeleteExpiredSAMLData extends CronTask {
public static final String RESOURCE_KEY = "io.supertokens.cronjobs.deleteExpiredSAMLData" +
".DeleteExpiredSAMLData";
private DeleteExpiredSAMLData(Main main, List<List<TenantIdentifier>> tenantsInfo) {
super("DeleteExpiredSAMLData", main, tenantsInfo, false);
}
public static DeleteExpiredSAMLData init(Main main, List<List<TenantIdentifier>> tenantsInfo) {
return (DeleteExpiredSAMLData) main.getResourceDistributor()
.setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY,
new DeleteExpiredSAMLData(main, tenantsInfo));
}
@Override
protected void doTaskPerStorage(Storage storage) throws Exception {
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
samlStorage.removeExpiredSAMLCodesAndRelayStates();
}
@Override
public int getIntervalTimeSeconds() {
if (Main.isTesting) {
Integer interval = CronTaskTest.getInstance(main).getIntervalInSeconds(RESOURCE_KEY);
if (interval != null) {
return interval;
}
}
// Every hour
return 3600;
}
@Override
public int getInitialWaitTimeSeconds() {
if (!Main.isTesting) {
return getIntervalTimeSeconds();
} else {
return 0;
}
}
}

View File

@ -7,6 +7,7 @@ import io.supertokens.cronjobs.CronTaskTest;
import io.supertokens.output.Logging;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage;
import io.supertokens.storageLayer.StorageLayer;
import org.jetbrains.annotations.TestOnly;
@ -30,7 +31,11 @@ public class DeleteExpiredTotpTokens extends CronTask {
@TestOnly
public static DeleteExpiredTotpTokens getInstance(Main main) {
return (DeleteExpiredTotpTokens) main.getResourceDistributor().getResource(RESOURCE_KEY);
try {
return (DeleteExpiredTotpTokens) main.getResourceDistributor().getResource(TenantIdentifier.BASE_TENANT, RESOURCE_KEY);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
}
@Override

View File

@ -19,12 +19,9 @@ package io.supertokens.cronjobs.syncCoreConfigWithDb;
import io.supertokens.Main;
import io.supertokens.cronjobs.CronTask;
import io.supertokens.cronjobs.CronTaskTest;
import io.supertokens.cronjobs.deleteExpiredSessions.DeleteExpiredSessions;
import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.multitenancy.MultitenancyHelper;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import java.util.List;
import io.supertokens.pluginInterface.opentelemetry.WithinOtelSpan;
public class SyncCoreConfigWithDb extends CronTask {
@ -62,6 +59,7 @@ public class SyncCoreConfigWithDb extends CronTask {
return 60;
}
@WithinOtelSpan
@Override
protected void doTaskForTargetTenant(TenantIdentifier targetTenant) throws Exception {
MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true);

View File

@ -157,7 +157,7 @@ public class Telemetry extends CronTask {
json.add("maus", new JsonArray());
}
String url = "https://api.supertokens.io/0/st/telemetry";
String url = "https://api.supertokens.com/0/st/telemetry";
// we call the API only if we are not testing the core, of if the request can be mocked (in case a test
// wants

View File

@ -17,6 +17,7 @@
package io.supertokens.dashboard;
import io.supertokens.Main;
import io.supertokens.ResourceDistributor;
import io.supertokens.dashboard.exceptions.UserSuspendedException;
import io.supertokens.emailpassword.PasswordHashing;
import io.supertokens.featureflag.EE_FEATURES;
@ -55,7 +56,7 @@ public class Dashboard {
throws StorageQueryException, DuplicateEmailException, FeatureNotEnabledException {
try {
Storage storage = StorageLayer.getStorage(main);
return signUpDashboardUser(new AppIdentifier(null, null), storage,
return signUpDashboardUser(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
main, email, password);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -103,7 +104,7 @@ public class Dashboard {
public static DashboardUser[] getAllDashboardUsers(Main main)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getAllDashboardUsers(new AppIdentifier(null, null), storage, main);
return getAllDashboardUsers(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, main);
}
public static DashboardUser[] getAllDashboardUsers(AppIdentifier appIdentifier, Storage storage, Main main)
@ -127,7 +128,7 @@ public class Dashboard {
throws StorageQueryException, UserSuspendedException {
try {
Storage storage = StorageLayer.getStorage(main);
return signInDashboardUser(new AppIdentifier(null, null), storage,
return signInDashboardUser(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
main, email, password);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -159,7 +160,7 @@ public class Dashboard {
public static boolean deleteUserWithUserId(Main main, String userId)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return deleteUserWithUserId(new AppIdentifier(null, null), storage, userId);
return deleteUserWithUserId(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId);
}
public static boolean deleteUserWithUserId(AppIdentifier appIdentifier, Storage storage, String userId)
@ -201,7 +202,7 @@ public class Dashboard {
public static boolean deleteUserWithEmail(Main main, String email)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return deleteUserWithEmail(new AppIdentifier(null, null), storage, email);
return deleteUserWithEmail(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, email);
}
public static boolean deleteUserWithEmail(AppIdentifier appIdentifier, Storage storage, String email)
@ -223,7 +224,7 @@ public class Dashboard {
try {
Storage storage = StorageLayer.getStorage(main);
return updateUsersCredentialsWithUserId(
new AppIdentifier(null, null), storage, main, userId,
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, main, userId,
newEmail, newPassword);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -291,7 +292,7 @@ public class Dashboard {
public static DashboardUser getDashboardUserByEmail(Main main, String email)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getDashboardUserByEmail(new AppIdentifier(null, null), storage, email);
return getDashboardUserByEmail(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, email);
}
public static DashboardUser getDashboardUserByEmail(AppIdentifier appIdentifier, Storage storage, String email)
@ -305,7 +306,7 @@ public class Dashboard {
public static boolean revokeSessionWithSessionId(Main main, String sessionId)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return revokeSessionWithSessionId(new AppIdentifier(null, null), storage, sessionId);
return revokeSessionWithSessionId(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, sessionId);
}
public static boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, Storage storage, String sessionId)
@ -320,7 +321,7 @@ public class Dashboard {
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getAllDashboardSessionsForUser(
new AppIdentifier(null, null), storage, userId);
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId);
}
public static DashboardSessionInfo[] getAllDashboardSessionsForUser(AppIdentifier appIdentifier, Storage storage,
@ -390,7 +391,7 @@ public class Dashboard {
public static boolean isValidUserSession(Main main, String sessionId)
throws StorageQueryException, UserSuspendedException {
Storage storage = StorageLayer.getStorage(main);
return isValidUserSession(new AppIdentifier(null, null), storage, main, sessionId);
return isValidUserSession(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, main, sessionId);
}
public static boolean isValidUserSession(AppIdentifier appIdentifier, Storage storage, Main main, String sessionId)

View File

@ -16,7 +16,18 @@
package io.supertokens.emailpassword;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.jetbrains.annotations.TestOnly;
import io.supertokens.Main;
import io.supertokens.ResourceDistributor;
import io.supertokens.authRecipe.AuthRecipe;
import io.supertokens.config.Config;
import io.supertokens.config.CoreConfig;
@ -50,14 +61,6 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoun
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.utils.Utils;
import io.supertokens.webserver.WebserverAPI;
import org.jetbrains.annotations.TestOnly;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.List;
public class EmailPassword {
@ -74,7 +77,7 @@ public class EmailPassword {
@TestOnly
public static long getPasswordResetTokenLifetimeForTests(Main main) {
try {
return getPasswordResetTokenLifetime(new TenantIdentifier(null, null, null), main);
return getPasswordResetTokenLifetime(ResourceDistributor.getAppForTesting(), main);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -90,7 +93,7 @@ public class EmailPassword {
throws DuplicateEmailException, StorageQueryException {
try {
Storage storage = StorageLayer.getStorage(main);
return signUp(new TenantIdentifier(null, null, null), storage,
return signUp(ResourceDistributor.getAppForTesting(), storage,
main, email, password);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -157,7 +160,7 @@ public class EmailPassword {
Storage storage = StorageLayer.getStorage(main);
return importUserWithPasswordHash(
new TenantIdentifier(null, null, null), storage, main, email,
ResourceDistributor.getAppForTesting(), storage, main, email,
passwordHash, hashingAlgorithm);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -215,7 +218,7 @@ public class EmailPassword {
public static ImportUserResponse createUserWithPasswordHash(TenantIdentifier tenantIdentifier, Storage storage,
@Nonnull String email,
@Nonnull String passwordHash, @Nullable long timeJoined)
@Nonnull String passwordHash, long timeJoined)
throws StorageQueryException, DuplicateEmailException, TenantOrAppNotFoundException,
StorageTransactionLogicException {
EmailPasswordSQLStorage epStorage = StorageUtils.getEmailPasswordStorage(storage);
@ -276,7 +279,7 @@ public class EmailPassword {
try {
Storage storage = StorageLayer.getStorage(main);
return importUserWithPasswordHash(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
main, email, passwordHash, null);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -289,7 +292,7 @@ public class EmailPassword {
throws StorageQueryException, WrongCredentialsException {
try {
Storage storage = StorageLayer.getStorage(main);
return signIn(new TenantIdentifier(null, null, null), storage,
return signIn(ResourceDistributor.getAppForTesting(), storage,
main, email, password);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -353,7 +356,7 @@ public class EmailPassword {
try {
Storage storage = StorageLayer.getStorage(main);
return generatePasswordResetTokenBeforeCdi4_0(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
main, userId);
} catch (TenantOrAppNotFoundException | BadPermissionException | WebserverAPI.BadRequestException e) {
throw new IllegalStateException(e);
@ -366,7 +369,7 @@ public class EmailPassword {
try {
Storage storage = StorageLayer.getStorage(main);
return generatePasswordResetToken(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
main, userId, null);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -379,7 +382,7 @@ public class EmailPassword {
try {
Storage storage = StorageLayer.getStorage(main);
return generatePasswordResetToken(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
main, userId, email);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -456,7 +459,7 @@ public class EmailPassword {
StorageTransactionLogicException {
try {
Storage storage = StorageLayer.getStorage(main);
return resetPassword(new TenantIdentifier(null, null, null), storage,
return resetPassword(ResourceDistributor.getAppForTesting(), storage,
main, token, password);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -530,7 +533,7 @@ public class EmailPassword {
StorageTransactionLogicException {
try {
Storage storage = StorageLayer.getStorage(main);
return consumeResetPasswordToken(new TenantIdentifier(null, null, null), storage,
return consumeResetPasswordToken(ResourceDistributor.getAppForTesting(), storage,
token);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -628,7 +631,7 @@ public class EmailPassword {
UnknownUserIdException, DuplicateEmailException, EmailChangeNotAllowedException {
try {
Storage storage = StorageLayer.getStorage(main);
updateUsersEmailOrPassword(new AppIdentifier(null, null), storage,
updateUsersEmailOrPassword(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
main, userId, email, password);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -725,7 +728,7 @@ public class EmailPassword {
throws StorageQueryException {
try {
Storage storage = StorageLayer.getStorage(main);
return getUserUsingId(new AppIdentifier(null, null), storage, userId);
return getUserUsingId(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}

View File

@ -30,6 +30,8 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoun
import org.jetbrains.annotations.TestOnly;
import org.mindrot.jbcrypt.BCrypt;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@ -42,6 +44,9 @@ public class PasswordHashing extends ResourceDistributor.SingletonResource {
final BlockingQueue<Object> firebaseSCryptBoundedQueue;
final Main main;
private final Map<String, String> cachedPasswordHashForTesting = new HashMap<>();
public static boolean bypassHashCachingInTesting = false;
private PasswordHashing(Main main) {
this.argon2BoundedQueue = new LinkedBlockingQueue<>(
Config.getBaseConfig(main).getArgon2HashingPoolSize());
@ -75,7 +80,7 @@ public class PasswordHashing extends ResourceDistributor.SingletonResource {
@TestOnly
public String createHashWithSalt(String password) {
try {
return createHashWithSalt(new AppIdentifier(null, null), password);
return createHashWithSalt(ResourceDistributor.getAppForTesting().toAppIdentifier(), password);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -84,6 +89,10 @@ public class PasswordHashing extends ResourceDistributor.SingletonResource {
public String createHashWithSalt(AppIdentifier appIdentifier, String password)
throws TenantOrAppNotFoundException {
if (Main.isTesting && !bypassHashCachingInTesting && cachedPasswordHashForTesting.containsKey(password)) {
return cachedPasswordHashForTesting.get(password);
}
String passwordHash = "";
TenantIdentifier tenantIdentifier = appIdentifier.getAsPublicTenantIdentifier();
@ -108,6 +117,10 @@ public class PasswordHashing extends ResourceDistributor.SingletonResource {
} catch (UnsupportedPasswordHashingFormatException e) {
throw new IllegalStateException(e);
}
if (Main.isTesting) {
cachedPasswordHashForTesting.put(password, passwordHash);
}
return passwordHash;
}

View File

@ -23,7 +23,7 @@ import io.supertokens.config.CoreConfig;
import io.supertokens.emailpassword.exceptions.UnsupportedPasswordHashingFormatException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import org.apache.tomcat.util.codec.binary.Base64;
import org.apache.commons.codec.binary.Base64;
import javax.annotation.Nullable;
import javax.crypto.Cipher;
@ -118,9 +118,9 @@ public class PasswordHashingUtils {
// concatenating decoded salt + separator
byte[] byteArrTemp = response.salt.getBytes(StandardCharsets.US_ASCII);
byte[] decodedSaltBytes = Base64.decodeBase64(byteArrTemp, 0, byteArrTemp.length);
byte[] decodedSaltBytes = Base64.decodeBase64(byteArrTemp);
byteArrTemp = response.saltSeparator.getBytes(StandardCharsets.US_ASCII);
byte[] decodedSaltSepBytes = Base64.decodeBase64(byteArrTemp, 0, byteArrTemp.length);
byte[] decodedSaltSepBytes = Base64.decodeBase64(byteArrTemp);
byte[] saltConcat = new byte[decodedSaltBytes.length + decodedSaltSepBytes.length];
System.arraycopy(decodedSaltBytes, 0, saltConcat, 0, decodedSaltBytes.length);
@ -136,7 +136,7 @@ public class PasswordHashingUtils {
}
// encrypting with aes
byteArrTemp = base64_signer_key.getBytes(StandardCharsets.US_ASCII);
byte[] signerBytes = Base64.decodeBase64(byteArrTemp, 0, byteArrTemp.length);
byte[] signerBytes = Base64.decodeBase64(byteArrTemp);
try {
String CIPHER = "AES/CTR/NoPadding";

View File

@ -17,6 +17,7 @@
package io.supertokens.emailverification;
import io.supertokens.Main;
import io.supertokens.ResourceDistributor;
import io.supertokens.config.Config;
import io.supertokens.emailverification.exception.EmailAlreadyVerifiedException;
import io.supertokens.emailverification.exception.EmailVerificationInvalidTokenException;
@ -44,7 +45,7 @@ public class EmailVerification {
public static long getEmailVerificationTokenLifetimeForTests(Main main) {
try {
return getEmailVerificationTokenLifetime(
new TenantIdentifier(null, null, null), main);
ResourceDistributor.getAppForTesting(), main);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -62,7 +63,7 @@ public class EmailVerification {
try {
Storage storage = StorageLayer.getStorage(main);
return generateEmailVerificationToken(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
main, userId, email);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -107,7 +108,7 @@ public class EmailVerification {
EmailVerificationInvalidTokenException, NoSuchAlgorithmException, StorageTransactionLogicException {
try {
Storage storage = StorageLayer.getStorage(main);
return verifyEmail(new TenantIdentifier(null, null, null), storage, token);
return verifyEmail(ResourceDistributor.getAppForTesting(), storage, token);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -182,7 +183,7 @@ public class EmailVerification {
public static boolean isEmailVerified(Main main, String userId,
String email) throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return isEmailVerified(new AppIdentifier(null, null), storage,
return isEmailVerified(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
userId, email);
}
@ -196,7 +197,7 @@ public class EmailVerification {
public static void revokeAllTokens(Main main, String userId,
String email) throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
revokeAllTokens(new TenantIdentifier(null, null, null), storage,
revokeAllTokens(ResourceDistributor.getAppForTesting(), storage,
userId, email);
}
@ -211,7 +212,7 @@ public class EmailVerification {
String email) throws StorageQueryException {
try {
Storage storage = StorageLayer.getStorage(main);
unverifyEmail(new AppIdentifier(null, null), storage, userId, email);
unverifyEmail(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId, email);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -249,7 +250,7 @@ public class EmailVerification {
try {
StorageUtils.getEmailVerificationStorage(StorageLayer.getStorage(main))
.addEmailVerificationToken(new TenantIdentifier(null, null, null),
.addEmailVerificationToken(ResourceDistributor.getAppForTesting(),
new EmailVerificationTokenInfo(userId, hashedToken,
System.currentTimeMillis() +
EmailVerification.getEmailVerificationTokenLifetimeForTests(main), email));

View File

@ -18,7 +18,7 @@ package io.supertokens.featureflag;
public enum EE_FEATURES {
ACCOUNT_LINKING("account_linking"), MULTI_TENANCY("multi_tenancy"), TEST("test"),
DASHBOARD_LOGIN("dashboard_login"), MFA("mfa"), SECURITY("security"), OAUTH("oauth");
DASHBOARD_LOGIN("dashboard_login"), MFA("mfa"), SECURITY("security"), OAUTH("oauth"), SAML("saml");
private final String name;

View File

@ -108,7 +108,7 @@ public class FeatureFlag extends ResourceDistributor.SingletonResource {
public static FeatureFlag getInstance(Main main) {
try {
return (FeatureFlag) main.getResourceDistributor()
.getResource(new AppIdentifier(null, null), RESOURCE_KEY);
.getResource(ResourceDistributor.getAppForTesting(), RESOURCE_KEY);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}

View File

@ -65,11 +65,16 @@ import io.supertokens.pluginInterface.oauth.OAuthLogoutChallenge;
import io.supertokens.pluginInterface.oauth.OAuthStorage;
import io.supertokens.pluginInterface.oauth.exception.DuplicateOAuthLogoutChallengeException;
import io.supertokens.pluginInterface.oauth.exception.OAuthClientNotFoundException;
import io.supertokens.pluginInterface.opentelemetry.OtelProvider;
import io.supertokens.pluginInterface.passwordless.PasswordlessCode;
import io.supertokens.pluginInterface.passwordless.PasswordlessDevice;
import io.supertokens.pluginInterface.passwordless.PasswordlessImportUser;
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;
import io.supertokens.pluginInterface.saml.SAMLRelayStateInfo;
import io.supertokens.pluginInterface.saml.SAMLStorage;
import io.supertokens.pluginInterface.session.SessionInfo;
import io.supertokens.pluginInterface.session.SessionStorage;
import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage;
@ -96,6 +101,11 @@ import io.supertokens.pluginInterface.userroles.UserRolesStorage;
import io.supertokens.pluginInterface.userroles.exception.DuplicateUserRoleMappingException;
import io.supertokens.pluginInterface.userroles.exception.UnknownRoleException;
import io.supertokens.pluginInterface.userroles.sqlStorage.UserRolesSQLStorage;
import io.supertokens.pluginInterface.webauthn.AccountRecoveryTokenInfo;
import io.supertokens.pluginInterface.webauthn.WebAuthNOptions;
import io.supertokens.pluginInterface.webauthn.WebAuthNStoredCredential;
import io.supertokens.pluginInterface.webauthn.exceptions.*;
import io.supertokens.pluginInterface.webauthn.slqStorage.WebAuthNSQLStorage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import org.sqlite.SQLiteException;
@ -111,7 +121,8 @@ public class Start
implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage,
JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage,
UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, TOTPSQLStorage, ActiveUsersStorage,
ActiveUsersSQLStorage, DashboardSQLStorage, AuthRecipeSQLStorage, OAuthStorage {
ActiveUsersSQLStorage, DashboardSQLStorage, AuthRecipeSQLStorage, OAuthStorage, WebAuthNSQLStorage,
SAMLStorage {
private static final Object appenderLock = new Object();
private static final String ACCESS_TOKEN_SIGNING_KEY_NAME = "access_token_signing_key";
@ -197,7 +208,7 @@ public class Start
}
@Override
public void initFileLogging(String infoLogPath, String errorLogPath) {
public void initFileLogging(String infoLogPath, String errorLogPath, OtelProvider otelProvider) {
// no op
}
@ -222,7 +233,7 @@ public class Start
@Override
public <T> T startTransaction(TransactionLogic<T> logic)
throws StorageTransactionLogicException, StorageQueryException {
return startTransaction(logic, TransactionIsolationLevel.SERIALIZABLE);
return startTransaction(logic, TransactionIsolationLevel.READ_COMMITTED);
}
@Override
@ -614,6 +625,11 @@ public class Start
return true;
}
@Override
public void updateConfigJsonFromEnv(JsonObject configJson) {
// do nothing
}
@Override
public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, String className, String userId)
throws StorageQueryException {
@ -754,6 +770,8 @@ public class Start
//ignore
} else if (className.equals(OAuthStorage.class.getName())) {
/* Since OAuth tables store client-related data, we don't add user-specific data here */
} else if (className.equals(SAMLStorage.class.getName())) {
// no user specific data here
} else if (className.equals(ActiveUsersStorage.class.getName())) {
try {
ActiveUsersQueries.updateUserLastActive(this, tenantIdentifier.toAppIdentifier(), userId);
@ -1011,7 +1029,8 @@ public class Start
@Override
public void updateMultipleIsEmailVerified_Transaction(AppIdentifier appIdentifier, TransactionConnection con,
Map<String, String> emailToUserId, boolean isEmailVerified)
Map<String, String> emailToUserId,
boolean isEmailVerified)
throws StorageQueryException, TenantOrAppNotFoundException {
Connection sqlCon = (Connection) con.getConnection();
try {
@ -1394,6 +1413,17 @@ public class Start
}
}
@Override
public AuthRecipeUserInfo getPrimaryUserByWebauthNCredentialId(TenantIdentifier tenantIdentifier,
String webauthNCredentialId)
throws StorageQueryException {
try {
return GeneralQueries.getPrimaryUserByWebauthNCredentialId(this, tenantIdentifier, webauthNCredentialId);
} catch (SQLException | StorageTransactionLogicException e) {
throw new StorageQueryException(e);
}
}
@Override
public AuthRecipeUserInfo getPrimaryUserByThirdPartyInfo(TenantIdentifier tenantIdentifier, String thirdPartyId,
String thirdPartyUserId) throws StorageQueryException {
@ -2732,7 +2762,7 @@ public class Start
try {
startTransaction(con -> {
try {
createDevice_Transaction(con, new AppIdentifier(null, null), device);
createDevice_Transaction(con, appIdentifier, device);
} catch (DeviceAlreadyExistsException | TenantOrAppNotFoundException e) {
throw new StorageTransactionLogicException(e);
}
@ -2987,6 +3017,19 @@ public class Start
}
}
@Override
public AuthRecipeUserInfo getPrimaryUserByWebauthNCredentialId_Transaction(TenantIdentifier tenantIdentifier,
TransactionConnection con,
String credentialId)
throws StorageQueryException {
try {
Connection sqlCon = (Connection) con.getConnection();
return GeneralQueries.getPrimaryUserByWebauthNCredentialId_Transaction(this, sqlCon, tenantIdentifier, credentialId);
} catch (SQLException | StorageTransactionLogicException e) {
throw new StorageQueryException(e);
}
}
@Override
public List<AuthRecipeUserInfo> getPrimaryUsersByIds_Transaction(AppIdentifier appIdentifier,
TransactionConnection con, List<String> userIds)
@ -3488,4 +3531,444 @@ public class Start
throw new StorageQueryException(e);
}
}
@Override
public WebAuthNStoredCredential saveCredentials(TenantIdentifier tenantIdentifier, WebAuthNStoredCredential credential)
throws StorageQueryException, DuplicateCredentialException,
io.supertokens.pluginInterface.webauthn.exceptions.UserIdNotFoundException, TenantOrAppNotFoundException {
try {
return WebAuthNQueries.saveCredential(this, tenantIdentifier, credential);
} catch (SQLException e) {
if (e instanceof SQLiteException) {
SQLiteConfig config = Config.getConfig(this);
String serverMessage = e.getMessage();
if (isPrimaryKeyError(serverMessage, config.getWebAuthNCredentialsTable(),
new String[]{"app_id", "user_id", "id"})) {
throw new io.supertokens.pluginInterface.webauthn.exceptions.DuplicateCredentialException();
} else if (isForeignKeyConstraintError(
serverMessage,
config.getWebAuthNUsersTable(),
new String[]{"user_id"},
new Object[]{tenantIdentifier.getAppId()})) {
throw new io.supertokens.pluginInterface.webauthn.exceptions.UserIdNotFoundException();
} else if (isForeignKeyConstraintError(
serverMessage,
config.getTenantsTable(),
new String[]{"app_id", "tenant_id"},
new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
throw new TenantOrAppNotFoundException(tenantIdentifier);
} else if (isForeignKeyConstraintError(
serverMessage,
config.getTenantsTable(),
new String[]{"app_id"},
new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
throw new TenantOrAppNotFoundException(tenantIdentifier);
}
}
throw new StorageQueryException(e);
}
}
@Override
public WebAuthNOptions saveGeneratedOptions(TenantIdentifier tenantIdentifier, WebAuthNOptions optionsToSave)
throws StorageQueryException, DuplicateOptionsIdException, TenantOrAppNotFoundException {
try {
return WebAuthNQueries.saveOptions(this, tenantIdentifier, optionsToSave);
} catch (SQLException e) {
if (e instanceof SQLiteException) {
SQLiteConfig config = Config.getConfig(this);
String serverMessage = e.getMessage();
if (isPrimaryKeyError(serverMessage, config.getWebAuthNGeneratedOptionsTable(),
new String[]{"app_id", "tenant_id", "id"})) {
throw new DuplicateOptionsIdException();
} else if (isForeignKeyConstraintError(
serverMessage,
config.getTenantsTable(),
new String[]{"app_id", "tenant_id"},
new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
throw new TenantOrAppNotFoundException(tenantIdentifier);
} else if (isForeignKeyConstraintError(
serverMessage,
config.getTenantsTable(),
new String[]{"app_id"},
new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
throw new TenantOrAppNotFoundException(tenantIdentifier);
}
}
throw new StorageQueryException(e);
}
}
@Override
public WebAuthNOptions loadOptionsById(TenantIdentifier tenantIdentifier, String optionsId)
throws StorageQueryException {
try {
return WebAuthNQueries.loadOptionsById(this, tenantIdentifier, optionsId);
} catch (SQLException e){
throw new StorageQueryException(e);
}
}
@Override
public WebAuthNStoredCredential loadCredentialByIdForUser(TenantIdentifier tenantIdentifier, String credentialId, String recipeUserId)
throws StorageQueryException {
try {
return WebAuthNQueries.loadCredentialByIdForUser(this, tenantIdentifier, credentialId, recipeUserId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public WebAuthNOptions loadOptionsById_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con,
String optionsId) throws StorageQueryException {
try {
Connection sqlCon = (Connection) con.getConnection();
return WebAuthNQueries.loadOptionsById_Transaction(this, sqlCon, tenantIdentifier, optionsId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public WebAuthNStoredCredential loadCredentialById_Transaction(TenantIdentifier tenantIdentifier,
TransactionConnection con, String credentialId)
throws StorageQueryException {
try {
Connection sqlCon = (Connection) con.getConnection();
return WebAuthNQueries.loadCredentialById_Transaction(this, sqlCon, tenantIdentifier, credentialId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public AuthRecipeUserInfo signUpWithCredentialsRegister_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con,
String userId, String email, String relyingPartyId, WebAuthNStoredCredential credential)
throws StorageQueryException, io.supertokens.pluginInterface.webauthn.exceptions.DuplicateUserIdException, TenantOrAppNotFoundException,
DuplicateUserEmailException {
Connection sqlCon = (Connection) con.getConnection();
try {
return WebAuthNQueries.signUpWithCredentialRegister_Transaction(this, sqlCon, tenantIdentifier, userId, email, relyingPartyId, credential);
} catch (StorageTransactionLogicException stle) {
if (stle.actualException instanceof SQLiteException) {
SQLiteConfig config = Config.getConfig(this);
String serverMessage = stle.actualException.getMessage();
if (isUniqueConstraintError(serverMessage, config.getWebAuthNUserToTenantTable(),
new String[]{"app_id", "tenant_id", "email"})) {
throw new DuplicateUserEmailException();
} else if (isPrimaryKeyError(serverMessage, config.getWebAuthNUsersTable(),
new String[]{"app_id", "user_id"})
|| isPrimaryKeyError(serverMessage, config.getUsersTable(),
new String[]{"app_id", "tenant_id", "user_id"})
|| isPrimaryKeyError(serverMessage, config.getWebAuthNUserToTenantTable(),
new String[]{"app_id", "tenant_id", "user_id"})
|| isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(),
new String[]{"app_id", "user_id"})) {
throw new io.supertokens.pluginInterface.webauthn.exceptions.DuplicateUserIdException();
} else if (isForeignKeyConstraintError(
serverMessage,
config.getAppsTable(),
new String[]{"app_id"},
new Object[]{tenantIdentifier.getAppId()})) {
throw new TenantOrAppNotFoundException(tenantIdentifier);
} else if (isForeignKeyConstraintError(
serverMessage,
config.getTenantsTable(),
new String[]{"app_id", "tenant_id"},
new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
throw new TenantOrAppNotFoundException(tenantIdentifier);
}
}
throw new StorageQueryException(stle.actualException);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public AuthRecipeUserInfo signUp_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con,
String userId, String email, String relyingPartyId)
throws StorageQueryException, TenantOrAppNotFoundException, DuplicateUserEmailException,
io.supertokens.pluginInterface.webauthn.exceptions.DuplicateUserIdException {
Connection sqlCon = (Connection) con.getConnection();
try {
return WebAuthNQueries.signUp_Transaction(this, sqlCon, tenantIdentifier, userId, email, relyingPartyId);
} catch (StorageTransactionLogicException stle) {
if (stle.actualException instanceof SQLiteException) {
SQLiteConfig config = Config.getConfig(this);
String serverMessage = stle.actualException.getMessage();
if (isUniqueConstraintError(serverMessage, config.getWebAuthNUserToTenantTable(),
new String[]{"app_id", "tenant_id", "email"})) {
throw new DuplicateUserEmailException();
} else if (isPrimaryKeyError(serverMessage, config.getWebAuthNUsersTable(),
new String[]{"app_id", "user_id"})
|| isPrimaryKeyError(serverMessage, config.getUsersTable(),
new String[]{"app_id", "tenant_id", "user_id"})
|| isPrimaryKeyError(serverMessage, config.getWebAuthNUserToTenantTable(),
new String[]{"app_id", "tenant_id", "user_id"})
|| isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(),
new String[]{"app_id", "user_id"})) {
throw new io.supertokens.pluginInterface.webauthn.exceptions.DuplicateUserIdException();
} else if (isForeignKeyConstraintError(
serverMessage,
config.getAppsTable(),
new String[]{"app_id"},
new Object[]{tenantIdentifier.getAppId()})) {
throw new TenantOrAppNotFoundException(tenantIdentifier);
} else if (isForeignKeyConstraintError(
serverMessage,
config.getTenantsTable(),
new String[]{"app_id", "tenant_id"},
new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
throw new TenantOrAppNotFoundException(tenantIdentifier);
}
}
throw new StorageQueryException(stle.actualException);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public AuthRecipeUserInfo getUserInfoByCredentialId_Transaction(TenantIdentifier tenantIdentifier,
TransactionConnection con, String credentialId)
throws StorageQueryException {
try {
Connection sqlCon = (Connection) con.getConnection();
return WebAuthNQueries.getUserInfoByCredentialId_Transaction(this, sqlCon, tenantIdentifier, credentialId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public void updateCounter_Transaction(TenantIdentifier tenantIdentifier,
TransactionConnection con, String credentialId,
long counter) throws StorageQueryException {
try {
Connection sqlCon = (Connection) con.getConnection();
WebAuthNQueries.updateCounter_Transaction(this, sqlCon, tenantIdentifier, credentialId, counter);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public void addRecoverAccountToken(TenantIdentifier tenantIdentifier, AccountRecoveryTokenInfo accountRecoveryTokenInfo)
throws DuplicateRecoverAccountTokenException, StorageQueryException {
try {
WebAuthNQueries.addRecoverAccountToken(this, tenantIdentifier, accountRecoveryTokenInfo);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public void removeCredential(TenantIdentifier tenantIdentifier, String userId, String credentialId)
throws StorageQueryException, WebauthNCredentialNotExistsException {
try {
int rowsUpdated = WebAuthNQueries.removeCredential(this, tenantIdentifier, userId, credentialId);
if(rowsUpdated < 1) {
throw new WebauthNCredentialNotExistsException();
}
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public void removeOptions(TenantIdentifier tenantIdentifier, String optionsId)
throws StorageQueryException, WebauthNOptionsNotExistsException {
try {
int rowsUpdated = WebAuthNQueries.removeOptions(this, tenantIdentifier, optionsId);
if(rowsUpdated < 1) {
throw new WebauthNOptionsNotExistsException();
}
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public List<WebAuthNStoredCredential> listCredentialsForUser(TenantIdentifier tenantIdentifier, String userId)
throws StorageQueryException {
try {
return WebAuthNQueries.listCredentials(this, tenantIdentifier, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public void updateUserEmail(TenantIdentifier tenantIdentifier, String userId, String newEmail)
throws StorageQueryException, io.supertokens.pluginInterface.webauthn.exceptions.UserIdNotFoundException,
DuplicateUserEmailException {
try {
WebAuthNQueries.updateUserEmail(this, tenantIdentifier, userId, newEmail);
} catch (StorageQueryException e) {
if (e.getCause() instanceof SQLiteException){
String errorMessage = e.getCause().getMessage();
SQLiteConfig config = Config.getConfig(this);
if (isUniqueConstraintError(errorMessage, config.getWebAuthNUserToTenantTable(),
new String[]{"app_id", "tenant_id", "email"})) {
throw new DuplicateUserEmailException();
} else if (isForeignKeyConstraintError(
errorMessage,
config.getWebAuthNUserToTenantTable(),
new String[]{"app_id", "tenant_id", "user_id"},
new Object[]{tenantIdentifier.getAppId(), userId})) {
throw new io.supertokens.pluginInterface.webauthn.exceptions.UserIdNotFoundException();
}
}
throw new StorageQueryException(e);
}
}
@Override
public void updateUserEmail_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId,
String newEmail)
throws StorageQueryException, io.supertokens.pluginInterface.webauthn.exceptions.UserIdNotFoundException,
DuplicateUserEmailException {
try {
Connection sqlCon = (Connection) con.getConnection();
WebAuthNQueries.updateUserEmail_Transaction(this, sqlCon, tenantIdentifier, userId, newEmail);
} catch (StorageQueryException e) {
if (e.getCause() instanceof SQLiteException){
String errorMessage = e.getCause().getMessage();
SQLiteConfig config = Config.getConfig(this);
if (isUniqueConstraintError(errorMessage, config.getWebAuthNUserToTenantTable(),
new String[]{"app_id", "tenant_id", "email"})) {
throw new DuplicateUserEmailException();
} else if (isForeignKeyConstraintError(
errorMessage,
config.getWebAuthNUserToTenantTable(),
new String[]{"app_id", "tenant_id", "user_id"},
new Object[]{tenantIdentifier.getAppId(), userId})) {
throw new io.supertokens.pluginInterface.webauthn.exceptions.UserIdNotFoundException();
}
}
throw new StorageQueryException(e);
}
}
@Override
public AccountRecoveryTokenInfo getAccountRecoveryTokenInfoByToken_Transaction(TenantIdentifier tenantIdentifier,
TransactionConnection con,
String token)
throws StorageQueryException {
Connection sqlCon = (Connection) con.getConnection();
try {
return WebAuthNQueries.getAccountRecoveryTokenInfoByToken_Transaction(this, tenantIdentifier, sqlCon, token);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public void deleteAccountRecoveryTokenByEmail_Transaction(TenantIdentifier tenantIdentifier,
TransactionConnection con, String email)
throws StorageQueryException {
Connection sqlCon = (Connection) con.getConnection();
try {
WebAuthNQueries.deleteAccountRecoveryTokenByEmail_Transaction(this, sqlCon, tenantIdentifier, email);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public void deleteExpiredAccountRecoveryTokens() throws StorageQueryException {
try {
WebAuthNQueries.deleteExpiredAccountRecoveryTokens(this);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public void deleteExpiredGeneratedOptions() throws StorageQueryException {
try {
WebAuthNQueries.deleteExpiredGeneratedOptions(this);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
@Override
public SAMLClient createOrUpdateSAMLClient(TenantIdentifier tenantIdentifier, SAMLClient samlClient)
throws StorageQueryException, io.supertokens.pluginInterface.saml.exception.DuplicateEntityIdException {
try {
return SAMLQueries.createOrUpdateSAMLClient(this, tenantIdentifier, samlClient.clientId, samlClient.clientSecret,
samlClient.ssoLoginURL, samlClient.redirectURIs.toString(), samlClient.defaultRedirectURI,
samlClient.idpEntityId, samlClient.idpSigningCertificate, samlClient.allowIDPInitiatedLogin,
samlClient.enableRequestSigning);
} catch (SQLException e) {
String errorMessage = e.getMessage();
String table = io.supertokens.inmemorydb.config.Config.getConfig(this).getSAMLClientsTable();
if (isUniqueConstraintError(errorMessage, table, new String[]{"app_id", "tenant_id", "idp_entity_id"})) {
throw new io.supertokens.pluginInterface.saml.exception.DuplicateEntityIdException();
}
throw new StorageQueryException(e);
}
}
@Override
public boolean removeSAMLClient(TenantIdentifier tenantIdentifier, String clientId) throws StorageQueryException {
return SAMLQueries.removeSAMLClient(this, tenantIdentifier, clientId);
}
@Override
public SAMLClient getSAMLClient(TenantIdentifier tenantIdentifier, String clientId) throws StorageQueryException {
return SAMLQueries.getSAMLClient(this, tenantIdentifier, clientId);
}
@Override
public SAMLClient getSAMLClientByIDPEntityId(TenantIdentifier tenantIdentifier, String idpEntityId) throws StorageQueryException {
return SAMLQueries.getSAMLClientByIDPEntityId(this, tenantIdentifier, idpEntityId);
}
@Override
public List<SAMLClient> getSAMLClients(TenantIdentifier tenantIdentifier) throws StorageQueryException {
return SAMLQueries.getSAMLClients(this, tenantIdentifier);
}
@Override
public void saveRelayStateInfo(TenantIdentifier tenantIdentifier, SAMLRelayStateInfo relayStateInfo, long relayStateValidity) throws StorageQueryException {
SAMLQueries.saveRelayStateInfo(this, tenantIdentifier, relayStateInfo.relayState, relayStateInfo.clientId, relayStateInfo.state, relayStateInfo.redirectURI, relayStateValidity);
}
@Override
public SAMLRelayStateInfo getRelayStateInfo(TenantIdentifier tenantIdentifier, String relayState) throws StorageQueryException {
return SAMLQueries.getRelayStateInfo(this, tenantIdentifier, relayState);
}
@Override
public void saveSAMLClaims(TenantIdentifier tenantIdentifier, String clientId, String code, JsonObject claims, long claimsValidity) throws StorageQueryException {
SAMLQueries.saveSAMLClaims(this, tenantIdentifier, clientId, code, claims.toString(), claimsValidity);
}
@Override
public SAMLClaimsInfo getSAMLClaimsAndRemoveCode(TenantIdentifier tenantIdentifier, String code) throws StorageQueryException {
return SAMLQueries.getSAMLClaimsAndRemoveCode(this, tenantIdentifier, code);
}
@Override
public void removeExpiredSAMLCodesAndRelayStates() throws StorageQueryException {
SAMLQueries.removeExpiredSAMLCodesAndRelayStates(this);
}
@Override
public int countSAMLClients(TenantIdentifier tenantIdentifier) throws StorageQueryException {
return SAMLQueries.countSAMLClients(this, tenantIdentifier);
}
}

View File

@ -184,4 +184,20 @@ public class SQLiteConfig {
public String getOAuthLogoutChallengesTable() {
return "oauth_logout_challenges";
}
public String getWebAuthNUsersTable(){ return "webauthn_users";}
public String getWebAuthNUserToTenantTable(){ return "webauthn_user_to_tenant"; }
public String getWebAuthNGeneratedOptionsTable() { return "webauthn_generated_options"; }
public String getWebAuthNCredentialsTable() { return "webauthn_credentials"; }
public String getWebAuthNAccountRecoveryTokenTable() { return "webauthn_account_recovery_tokens"; }
public String getSAMLClientsTable() { return "saml_clients"; }
public String getSAMLRelayStateTable() { return "saml_relay_state"; }
public String getSAMLClaimsTable() { return "saml_claims"; }
}

View File

@ -51,6 +51,11 @@ public class EmailVerificationQueries {
+ ");";
}
static String getQueryToCreateEmailVerificationVerifiedEmailsAppIdIndex(Start start) {
return "CREATE INDEX emailverification_verified_emails_verified_appid_emails_index ON "
+ Config.getConfig(start).getEmailVerificationTable() + "(app_id, email);";
}
static String getQueryToCreateEmailVerificationTokensTable(Start start) {
return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getEmailVerificationTokensTable() + " ("
+ "app_id VARCHAR(64) DEFAULT 'public',"

View File

@ -26,6 +26,7 @@ import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.authRecipe.LoginMethod;
import io.supertokens.pluginInterface.dashboard.DashboardSearchTags;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import org.jetbrains.annotations.NotNull;
@ -317,6 +318,9 @@ public class GeneralQueries {
if (!doesTableExists(start, Config.getConfig(start).getEmailVerificationTable())) {
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, getQueryToCreateEmailVerificationTable(start), NO_OP_SETTER);
//index
update(start, getQueryToCreateEmailVerificationVerifiedEmailsAppIdIndex(start), NO_OP_SETTER);
}
if (!doesTableExists(start, Config.getConfig(start).getEmailVerificationTokensTable())) {
@ -406,6 +410,7 @@ public class GeneralQueries {
// index
update(start, UserRolesQueries.getQueryToCreateUserRolesRoleIndex(start), NO_OP_SETTER);
update(start, UserRolesQueries.getQueryToCreateUserRolesUserIdAppIdIndex(start), NO_OP_SETTER);
}
if (!doesTableExists(start, Config.getConfig(start).getUserIdMappingTable())) {
@ -473,6 +478,71 @@ public class GeneralQueries {
// index
update(start, OAuthQueries.getQueryToCreateOAuthLogoutChallengesTimeCreatedIndex(start), NO_OP_SETTER);
}
if(!doesTableExists(start, Config.getConfig(start).getWebAuthNUsersTable())){
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, WebAuthNQueries.getQueryToCreateWebAuthNUsersTable(start), NO_OP_SETTER);
}
if(!doesTableExists(start, Config.getConfig(start).getWebAuthNUserToTenantTable())){
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, WebAuthNQueries.getQueryToCreateWebAuthNUsersToTenantTable(start), NO_OP_SETTER);
//index
update(start, WebAuthNQueries.getQueryToCreateWebAuthNUserToTenantEmailIndex(start), NO_OP_SETTER);
}
if(!doesTableExists(start, Config.getConfig(start).getWebAuthNGeneratedOptionsTable())){
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, WebAuthNQueries.getQueryToCreateWebAuthNGeneratedOptionsTable(start), NO_OP_SETTER);
//index
update(start, WebAuthNQueries.getQueryToCreateWebAuthNChallengeExpiresIndex(start), NO_OP_SETTER);
}
if(!doesTableExists(start, Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable())){
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, WebAuthNQueries.getQueryToCreateWebAuthNAccountRecoveryTokenTable(start), NO_OP_SETTER);
//index
update(start, WebAuthNQueries.getQueryToCreateWebAuthNAccountRecoveryTokenTokenIndex(start), NO_OP_SETTER);
update(start, WebAuthNQueries.getQueryToCreateWebAuthNAccountRecoveryTokenEmailIndex(start), NO_OP_SETTER);
update(start, WebAuthNQueries.getQueryToCreateWebAuthNAccountRecoveryTokenExpiresAtIndex(start), NO_OP_SETTER);
}
if(!doesTableExists(start, Config.getConfig(start).getWebAuthNCredentialsTable())){
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, WebAuthNQueries.getQueryToCreateWebAuthNCredentialsTable(start), NO_OP_SETTER);
//index
update(start, WebAuthNQueries.getQueryToCreateWebAuthNCredentialsUserIdIndex(start), NO_OP_SETTER);
}
// SAML tables
if (!doesTableExists(start, Config.getConfig(start).getSAMLClientsTable())) {
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, SAMLQueries.getQueryToCreateSAMLClientsTable(start), NO_OP_SETTER);
// indexes
update(start, SAMLQueries.getQueryToCreateSAMLClientsAppIdTenantIdIndex(start), NO_OP_SETTER);
}
if (!doesTableExists(start, Config.getConfig(start).getSAMLRelayStateTable())) {
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, SAMLQueries.getQueryToCreateSAMLRelayStateTable(start), NO_OP_SETTER);
// indexes
update(start, SAMLQueries.getQueryToCreateSAMLRelayStateAppIdTenantIdIndex(start), NO_OP_SETTER);
update(start, SAMLQueries.getQueryToCreateSAMLRelayStateExpiresAtIndex(start), NO_OP_SETTER);
}
if (!doesTableExists(start, Config.getConfig(start).getSAMLClaimsTable())) {
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, SAMLQueries.getQueryToCreateSAMLClaimsTable(start), NO_OP_SETTER);
// indexes
update(start, SAMLQueries.getQueryToCreateSAMLClaimsAppIdTenantIdIndex(start), NO_OP_SETTER);
update(start, SAMLQueries.getQueryToCreateSAMLClaimsExpiresAtIndex(start), NO_OP_SETTER);
}
}
public static void setKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier,
@ -831,6 +901,45 @@ public class GeneralQueries {
}
}
{
// check if we should search through the webauthn table
if (dashboardSearchTags.shouldWebauthnTableBeSearched()) {
String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable()
+ " AS allAuthUsersTable" +
" JOIN " + getConfig(start).getWebAuthNUserToTenantTable()
+ " AS webauthnTable ON allAuthUsersTable.app_id = webauthnTable.app_id AND "
+ "allAuthUsersTable.tenant_id = webauthnTable.tenant_id AND "
+ "allAuthUsersTable.user_id = webauthnTable.user_id";
// attach email tags to queries
QUERY = QUERY +
" WHERE (webauthnTable.app_id = ? AND webauthnTable.tenant_id = ?) AND"
+ " ( webauthnTable.email LIKE ? OR webauthnTable.email LIKE ? ";
queryList.add(tenantIdentifier.getAppId());
queryList.add(tenantIdentifier.getTenantId());
queryList.add(dashboardSearchTags.emails.get(0) + "%");
queryList.add("%@" + dashboardSearchTags.emails.get(0) + "%");
for (int i = 1; i < dashboardSearchTags.emails.size(); i++) {
QUERY += " OR webauthnTable.email LIKE ? OR webauthnTable.email LIKE ?";
queryList.add(dashboardSearchTags.emails.get(i) + "%");
queryList.add("%@" + dashboardSearchTags.emails.get(i) + "%");
}
QUERY += " )";
// check if we need to append this to an existing search query
if (USER_SEARCH_TAG_CONDITION.length() != 0) {
USER_SEARCH_TAG_CONDITION.append(" UNION ").append("SELECT * FROM ( ").append(QUERY)
.append(" LIMIT 1000) AS webauthnResultTable");
} else {
USER_SEARCH_TAG_CONDITION.append("SELECT * FROM ( ").append(QUERY)
.append(" LIMIT 1000) AS webauthnResultTable");
}
}
}
if (USER_SEARCH_TAG_CONDITION.toString().length() == 0) {
usersFromQuery = new ArrayList<>();
} else {
@ -1193,6 +1302,13 @@ public class GeneralQueries {
userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail_Transaction(start, sqlCon, appIdentifier, email));
String webauthnUserId = WebAuthNQueries.getPrimaryUserIdForAppUsingEmail_Transaction(start, sqlCon,
appIdentifier, email);
if(webauthnUserId != null) {
userIds.add(webauthnUserId);
}
// remove duplicates from userIds
Set<String> userIdsSet = new HashSet<>(userIds);
userIds = new ArrayList<>(userIdsSet);
@ -1224,6 +1340,11 @@ public class GeneralQueries {
userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier, email));
String webauthnUserId = WebAuthNQueries.getPrimaryUserIdForTenantUsingEmail(start, tenantIdentifier, email);
if(webauthnUserId != null) {
userIds.add(webauthnUserId);
}
// remove duplicates from userIds
Set<String> userIdsSet = new HashSet<>(userIds);
userIds = new ArrayList<>(userIdsSet);
@ -1268,6 +1389,34 @@ public class GeneralQueries {
return getPrimaryUserInfoForUserId(start, tenantIdentifier.toAppIdentifier(), userId);
}
public static AuthRecipeUserInfo getPrimaryUserByWebauthNCredentialId(Start start,
TenantIdentifier tenantIdentifier,
String credentialId)
throws StorageQueryException, SQLException, StorageTransactionLogicException {
AuthRecipeUserInfo webauthnUser = start.startTransaction(con -> {
try {
Connection sqlCon = (Connection) con.getConnection();
return getPrimaryUserByWebauthNCredentialId_Transaction(start, sqlCon, tenantIdentifier,
credentialId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
});
return webauthnUser;
}
public static AuthRecipeUserInfo getPrimaryUserByWebauthNCredentialId_Transaction(Start start,
Connection connection,
TenantIdentifier tenantIdentifier,
String credentialId)
throws StorageQueryException, SQLException, StorageTransactionLogicException {
AuthRecipeUserInfo webauthnUser = WebAuthNQueries.getUserInfoByCredentialId_Transaction(start, connection,
tenantIdentifier, credentialId);
return getPrimaryUserInfoForUserId_Transaction(start, connection, tenantIdentifier.toAppIdentifier(),
webauthnUser.getSupertokensUserId());
}
public static String getPrimaryUserIdStrForUserId(Start start, AppIdentifier appIdentifier, String id)
throws SQLException, StorageQueryException {
String QUERY = "SELECT primary_or_recipe_user_id FROM " + getConfig(start).getUsersTable() +
@ -1381,6 +1530,7 @@ public class GeneralQueries {
loginMethods.addAll(ThirdPartyQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier));
loginMethods.addAll(
PasswordlessQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier));
loginMethods.addAll(WebAuthNQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier));
Map<String, LoginMethod> recipeUserIdToLoginMethodMap = new HashMap<>();
for (LoginMethod loginMethod : loginMethods) {
@ -1480,6 +1630,8 @@ public class GeneralQueries {
loginMethods.addAll(
PasswordlessQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch,
appIdentifier));
loginMethods.addAll(WebAuthNQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch,
appIdentifier));
Map<String, LoginMethod> recipeUserIdToLoginMethodMap = new HashMap<>();
for (LoginMethod loginMethod : loginMethods) {

View File

@ -0,0 +1,458 @@
/*
* Copyright (c) 2025, 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 java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
import io.supertokens.inmemorydb.Start;
import io.supertokens.inmemorydb.config.Config;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.saml.SAMLClaimsInfo;
import io.supertokens.pluginInterface.saml.SAMLClient;
import io.supertokens.pluginInterface.saml.SAMLRelayStateInfo;
public class SAMLQueries {
public static String getQueryToCreateSAMLClientsTable(Start start) {
String table = Config.getConfig(start).getSAMLClientsTable();
String tenantsTable = Config.getConfig(start).getTenantsTable();
// @formatter:off
return "CREATE TABLE IF NOT EXISTS " + table + " ("
+ "app_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "client_id VARCHAR(255) NOT NULL,"
+ "client_secret TEXT,"
+ "sso_login_url TEXT NOT NULL,"
+ "redirect_uris TEXT NOT NULL," // store JsonArray.toString()
+ "default_redirect_uri VARCHAR(1024) NOT NULL,"
+ "idp_entity_id VARCHAR(1024),"
+ "idp_signing_certificate TEXT,"
+ "allow_idp_initiated_login BOOLEAN NOT NULL DEFAULT FALSE,"
+ "enable_request_signing BOOLEAN NOT NULL DEFAULT TRUE,"
+ "created_at BIGINT NOT NULL,"
+ "updated_at BIGINT NOT NULL,"
+ "UNIQUE (app_id, tenant_id, idp_entity_id),"
+ "PRIMARY KEY (app_id, tenant_id, client_id),"
+ "FOREIGN KEY (app_id, tenant_id) REFERENCES " + tenantsTable + " (app_id, tenant_id) ON DELETE CASCADE"
+ ");";
// @formatter:on
}
public static String getQueryToCreateSAMLClientsAppIdTenantIdIndex(Start start) {
String table = Config.getConfig(start).getSAMLClientsTable();
return "CREATE INDEX IF NOT EXISTS saml_clients_app_tenant_index ON " + table + "(app_id, tenant_id);";
}
public static String getQueryToCreateSAMLRelayStateTable(Start start) {
String table = Config.getConfig(start).getSAMLRelayStateTable();
String tenantsTable = Config.getConfig(start).getTenantsTable();
// @formatter:off
return "CREATE TABLE IF NOT EXISTS " + table + " ("
+ "app_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "relay_state VARCHAR(255) NOT NULL,"
+ "client_id VARCHAR(255) NOT NULL,"
+ "state TEXT,"
+ "redirect_uri VARCHAR(1024) NOT NULL,"
+ "created_at BIGINT NOT NULL,"
+ "expires_at BIGINT NOT NULL,"
+ "PRIMARY KEY (relay_state)," // relayState must be unique
+ "FOREIGN KEY (app_id, tenant_id) REFERENCES " + tenantsTable + " (app_id, tenant_id) ON DELETE CASCADE"
+ ");";
// @formatter:on
}
public static String getQueryToCreateSAMLRelayStateAppIdTenantIdIndex(Start start) {
String table = Config.getConfig(start).getSAMLRelayStateTable();
return "CREATE INDEX IF NOT EXISTS saml_relay_state_app_tenant_index ON " + table + "(app_id, tenant_id);";
}
public static String getQueryToCreateSAMLRelayStateExpiresAtIndex(Start start) {
String table = Config.getConfig(start).getSAMLRelayStateTable();
return "CREATE INDEX IF NOT EXISTS saml_relay_state_expires_at_index ON " + table + "(expires_at);";
}
public static String getQueryToCreateSAMLClaimsTable(Start start) {
String table = Config.getConfig(start).getSAMLClaimsTable();
String tenantsTable = Config.getConfig(start).getTenantsTable();
// @formatter:off
return "CREATE TABLE IF NOT EXISTS " + table + " ("
+ "app_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "client_id VARCHAR(255) NOT NULL,"
+ "code VARCHAR(255) NOT NULL,"
+ "claims TEXT NOT NULL,"
+ "created_at BIGINT NOT NULL,"
+ "expires_at BIGINT NOT NULL,"
+ "PRIMARY KEY (code),"
+ "FOREIGN KEY (app_id, tenant_id) REFERENCES " + tenantsTable + " (app_id, tenant_id) ON DELETE CASCADE"
+ ");";
// @formatter:on
}
public static String getQueryToCreateSAMLClaimsAppIdTenantIdIndex(Start start) {
String table = Config.getConfig(start).getSAMLClaimsTable();
return "CREATE INDEX IF NOT EXISTS saml_claims_app_tenant_index ON " + table + "(app_id, tenant_id);";
}
public static String getQueryToCreateSAMLClaimsExpiresAtIndex(Start start) {
String table = Config.getConfig(start).getSAMLClaimsTable();
return "CREATE INDEX IF NOT EXISTS saml_claims_expires_at_index ON " + table + "(expires_at);";
}
public static void saveRelayStateInfo(Start start, TenantIdentifier tenantIdentifier,
String relayState, String clientId, String state, String redirectURI, long relayStateValidity)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLRelayStateTable();
String QUERY = "INSERT INTO " + table +
" (app_id, tenant_id, relay_state, client_id, state, redirect_uri, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
try {
update(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, relayState);
pst.setString(4, clientId);
if (state != null) {
pst.setString(5, state);
} else {
pst.setNull(5, java.sql.Types.VARCHAR);
}
pst.setString(6, redirectURI);
pst.setLong(7, System.currentTimeMillis());
pst.setLong(8, System.currentTimeMillis() + relayStateValidity);
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static SAMLRelayStateInfo getRelayStateInfo(Start start, TenantIdentifier tenantIdentifier, String relayState)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLRelayStateTable();
String QUERY = "SELECT client_id, state, redirect_uri, expires_at FROM " + table
+ " WHERE app_id = ? AND tenant_id = ? AND relay_state = ? AND expires_at >= ?";
try {
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, relayState);
pst.setLong(4, System.currentTimeMillis());
}, result -> {
if (result.next()) {
String clientId = result.getString("client_id");
String state = result.getString("state"); // may be null
String redirectURI = result.getString("redirect_uri");
return new SAMLRelayStateInfo(relayState, clientId, state, redirectURI);
}
return null;
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static void saveSAMLClaims(Start start, TenantIdentifier tenantIdentifier, String clientId, String code, String claimsJson, long claimsValidity)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClaimsTable();
String QUERY = "INSERT INTO " + table +
" (app_id, tenant_id, client_id, code, claims, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)";
try {
update(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, clientId);
pst.setString(4, code);
pst.setString(5, claimsJson);
pst.setLong(6, System.currentTimeMillis());
pst.setLong(7, System.currentTimeMillis() + claimsValidity);
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static SAMLClaimsInfo getSAMLClaimsAndRemoveCode(Start start, TenantIdentifier tenantIdentifier, String code)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClaimsTable();
String QUERY = "SELECT client_id, claims FROM " + table + " WHERE app_id = ? AND tenant_id = ? AND code = ? AND expires_at >= ?";
try {
SAMLClaimsInfo claimsInfo = execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, code);
pst.setLong(4, System.currentTimeMillis());
}, result -> {
if (result.next()) {
String clientId = result.getString("client_id");
JsonObject claims = com.google.gson.JsonParser.parseString(result.getString("claims")).getAsJsonObject();
return new SAMLClaimsInfo(clientId, claims);
}
return null;
});
if (claimsInfo != null) {
String DELETE = "DELETE FROM " + table + " WHERE app_id = ? AND tenant_id = ? AND code = ?";
update(start, DELETE, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, code);
});
}
return claimsInfo;
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static SAMLClient createOrUpdateSAMLClient(
Start start,
TenantIdentifier tenantIdentifier,
String clientId,
String clientSecret,
String ssoLoginURL,
String redirectURIsJson,
String defaultRedirectURI,
String idpEntityId,
String idpSigningCertificate,
boolean allowIDPInitiatedLogin,
boolean enableRequestSigning)
throws StorageQueryException, SQLException {
String table = Config.getConfig(start).getSAMLClientsTable();
String QUERY = "INSERT INTO " + table +
" (app_id, tenant_id, client_id, client_secret, sso_login_url, redirect_uris, default_redirect_uri, idp_entity_id, idp_signing_certificate, allow_idp_initiated_login, enable_request_signing, created_at, updated_at) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " +
"ON CONFLICT (app_id, tenant_id, client_id) DO UPDATE SET " +
"client_secret = ?, sso_login_url = ?, redirect_uris = ?, default_redirect_uri = ?, idp_entity_id = ?, idp_signing_certificate = ?, allow_idp_initiated_login = ?, enable_request_signing = ?, updated_at = ?";
long now = System.currentTimeMillis();
update(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, clientId);
if (clientSecret != null) {
pst.setString(4, clientSecret);
} else {
pst.setNull(4, Types.VARCHAR);
}
pst.setString(5, ssoLoginURL);
pst.setString(6, redirectURIsJson);
pst.setString(7, defaultRedirectURI);
if (idpEntityId != null) {
pst.setString(8, idpEntityId);
} else {
pst.setNull(8, java.sql.Types.VARCHAR);
}
if (idpSigningCertificate != null) {
pst.setString(9, idpSigningCertificate);
} else {
pst.setNull(9, Types.VARCHAR);
}
pst.setBoolean(10, allowIDPInitiatedLogin);
pst.setBoolean(11, enableRequestSigning);
pst.setLong(12, now);
pst.setLong(13, now);
if (clientSecret != null) {
pst.setString(14, clientSecret);
} else {
pst.setNull(14, Types.VARCHAR);
}
pst.setString(15, ssoLoginURL);
pst.setString(16, redirectURIsJson);
pst.setString(17, defaultRedirectURI);
if (idpEntityId != null) {
pst.setString(18, idpEntityId);
} else {
pst.setNull(18, java.sql.Types.VARCHAR);
}
if (idpSigningCertificate != null) {
pst.setString(19, idpSigningCertificate);
} else {
pst.setNull(19, Types.VARCHAR);
}
pst.setBoolean(20, allowIDPInitiatedLogin);
pst.setBoolean(21, enableRequestSigning);
pst.setLong(22, now);
});
return getSAMLClient(start, tenantIdentifier, clientId);
}
public static SAMLClient getSAMLClient(Start start, TenantIdentifier tenantIdentifier, String clientId)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClientsTable();
String QUERY = "SELECT client_id, client_secret, sso_login_url, redirect_uris, default_redirect_uri, idp_entity_id, idp_signing_certificate, allow_idp_initiated_login, enable_request_signing FROM " + table
+ " WHERE app_id = ? AND tenant_id = ? AND client_id = ?";
try {
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, clientId);
}, result -> {
if (result.next()) {
String fetchedClientId = result.getString("client_id");
String clientSecret = result.getString("client_secret");
String ssoLoginURL = result.getString("sso_login_url");
String redirectUrisJson = result.getString("redirect_uris");
String defaultRedirectURI = result.getString("default_redirect_uri");
String idpEntityId = result.getString("idp_entity_id");
String idpSigningCertificate = result.getString("idp_signing_certificate");
boolean allowIDPInitiatedLogin = result.getBoolean("allow_idp_initiated_login");
boolean enableRequestSigning = result.getBoolean("enable_request_signing");
JsonArray redirectURIs = JsonParser.parseString(redirectUrisJson).getAsJsonArray();
return new SAMLClient(fetchedClientId, clientSecret, ssoLoginURL, redirectURIs, defaultRedirectURI, idpEntityId, idpSigningCertificate, allowIDPInitiatedLogin, enableRequestSigning);
}
return null;
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static SAMLClient getSAMLClientByIDPEntityId(Start start, TenantIdentifier tenantIdentifier, String idpEntityId) throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClientsTable();
String QUERY = "SELECT client_id, client_secret, sso_login_url, redirect_uris, default_redirect_uri, idp_entity_id, idp_signing_certificate, allow_idp_initiated_login, enable_request_signing FROM " + table
+ " WHERE app_id = ? AND tenant_id = ? AND idp_entity_id = ?";
try {
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, idpEntityId);
}, result -> {
if (result.next()) {
String fetchedClientId = result.getString("client_id");
String clientSecret = result.getString("client_secret");
String ssoLoginURL = result.getString("sso_login_url");
String redirectUrisJson = result.getString("redirect_uris");
String defaultRedirectURI = result.getString("default_redirect_uri");
String fetchedIdpEntityId = result.getString("idp_entity_id");
String idpSigningCertificate = result.getString("idp_signing_certificate");
boolean allowIDPInitiatedLogin = result.getBoolean("allow_idp_initiated_login");
boolean enableRequestSigning = result.getBoolean("enable_request_signing");
JsonArray redirectURIs = JsonParser.parseString(redirectUrisJson).getAsJsonArray();
return new SAMLClient(fetchedClientId, clientSecret, ssoLoginURL, redirectURIs, defaultRedirectURI, fetchedIdpEntityId, idpSigningCertificate, allowIDPInitiatedLogin, enableRequestSigning);
}
return null;
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static List<SAMLClient> getSAMLClients(Start start, TenantIdentifier tenantIdentifier)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClientsTable();
String QUERY = "SELECT client_id, client_secret, sso_login_url, redirect_uris, default_redirect_uri, idp_entity_id, idp_signing_certificate, allow_idp_initiated_login, enable_request_signing FROM " + table
+ " WHERE app_id = ? AND tenant_id = ?";
try {
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
}, result -> {
List<SAMLClient> clients = new ArrayList<>();
while (result.next()) {
String fetchedClientId = result.getString("client_id");
String clientSecret = result.getString("client_secret");
String ssoLoginURL = result.getString("sso_login_url");
String redirectUrisJson = result.getString("redirect_uris");
String defaultRedirectURI = result.getString("default_redirect_uri");
String idpEntityId = result.getString("idp_entity_id");
String idpSigningCertificate = result.getString("idp_signing_certificate");
boolean allowIDPInitiatedLogin = result.getBoolean("allow_idp_initiated_login");
boolean enableRequestSigning = result.getBoolean("enable_request_signing");
JsonArray redirectURIs = JsonParser.parseString(redirectUrisJson).getAsJsonArray();
clients.add(new SAMLClient(fetchedClientId, clientSecret, ssoLoginURL, redirectURIs, defaultRedirectURI, idpEntityId, idpSigningCertificate, allowIDPInitiatedLogin, enableRequestSigning));
}
return clients;
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static boolean removeSAMLClient(Start start, TenantIdentifier tenantIdentifier, String clientId)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClientsTable();
String QUERY = "DELETE FROM " + table + " WHERE app_id = ? AND tenant_id = ? AND client_id = ?";
try {
return update(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, clientId);
}) > 0;
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static void removeExpiredSAMLCodesAndRelayStates(Start start) throws StorageQueryException {
try {
{
String QUERY = "DELETE FROM " + Config.getConfig(start).getSAMLClaimsTable() + " WHERE expires_at <= ?";
update(start, QUERY, pst -> {
pst.setLong(1, System.currentTimeMillis());
});
}
{
String QUERY = "DELETE FROM " + Config.getConfig(start).getSAMLRelayStateTable() + " WHERE expires_at <= ?";
update(start, QUERY, pst -> {
pst.setLong(1, System.currentTimeMillis());
});
}
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static int countSAMLClients(Start start, TenantIdentifier tenantIdentifier) throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClientsTable();
String QUERY = "SELECT COUNT(*) as c FROM " + table
+ " WHERE app_id = ? AND tenant_id = ?";
try {
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
}, result -> {
if (result.next()) {
return result.getInt("c");
}
return 0;
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
}

View File

@ -66,6 +66,11 @@ public class UserRolesQueries {
+ Config.getConfig(start).getUserRolesPermissionsTable() + "(app_id, permission);";
}
public static String getQueryToCreateUserRolesUserIdAppIdIndex(Start start) {
return "CREATE INDEX user_roles_app_id_user_id_index ON " + Config.getConfig(start).getUserRolesTable() +
"(app_id, user_id)";
}
public static String getQueryToCreateUserRolesTable(Start start) {
String tableName = Config.getConfig(start).getUserRolesTable();
// @formatter:off

View File

@ -0,0 +1,777 @@
/*
* 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.Utils;
import io.supertokens.inmemorydb.config.Config;
import io.supertokens.pluginInterface.RowMapper;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.authRecipe.LoginMethod;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.webauthn.AccountRecoveryTokenInfo;
import io.supertokens.pluginInterface.webauthn.WebAuthNOptions;
import io.supertokens.pluginInterface.webauthn.WebAuthNStoredCredential;
import org.jetbrains.annotations.Nullable;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
import static io.supertokens.inmemorydb.config.Config.getConfig;
import static io.supertokens.pluginInterface.RECIPE_ID.WEBAUTHN;
public class WebAuthNQueries {
public static String getQueryToCreateWebAuthNUsersTable(Start start){
return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getWebAuthNUsersTable() + "(" +
" app_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
" user_id CHAR(36) NOT NULL," +
" email VARCHAR(256) NOT NULL," +
" rp_id VARCHAR(256) NOT NULL," +
" time_joined BIGINT UNSIGNED NOT NULL," +
" CONSTRAINT webauthn_users_pkey PRIMARY KEY (app_id, user_id), " +
" CONSTRAINT webauthn_users_to_app_id_fkey " +
" FOREIGN KEY (app_id, user_id) REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() +
" (app_id, user_id) ON DELETE CASCADE " +
");";
}
public static String getQueryToCreateWebAuthNUsersToTenantTable(Start start){
return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getWebAuthNUserToTenantTable() +" (" +
" app_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
" tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
" user_id CHAR(36) NOT NULL," +
" email VARCHAR(256) NOT NULL," +
" CONSTRAINT webauthn_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email)," +
" CONSTRAINT webauthn_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id)," +
" CONSTRAINT webauthn_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) " +
" REFERENCES "+ Config.getConfig(start).getUsersTable()+" (app_id, tenant_id, user_id) on delete CASCADE" +
");";
}
public static String getQueryToCreateWebAuthNUserToTenantEmailIndex(Start start) {
return "CREATE INDEX webauthn_user_to_tenant_email_index ON " +
Config.getConfig(start).getWebAuthNUserToTenantTable() +
" (app_id, email);";
}
public static String getQueryToCreateWebAuthNGeneratedOptionsTable(Start start){
return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable() + "(" +
" app_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
" tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
" id CHAR(36) NOT NULL," +
" challenge VARCHAR(256) NOT NULL," +
" email VARCHAR(256)," +
" rp_id VARCHAR(256) NOT NULL," +
" rp_name VARCHAR(256) NOT NULL," +
" origin VARCHAR(256) NOT NULL," +
" expires_at BIGINT UNSIGNED NOT NULL," +
" created_at BIGINT UNSIGNED NOT NULL," +
" user_presence_required BOOLEAN DEFAULT FALSE NOT NULL," +
" user_verification VARCHAR(12) DEFAULT `preferred` NOT NULL," +
" CONSTRAINT webauthn_user_challenges_pkey PRIMARY KEY (app_id, tenant_id, id)," +
" CONSTRAINT webauthn_user_challenges_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) " +
" REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE" +
");";
}
public static String getQueryToCreateWebAuthNChallengeExpiresIndex(Start start) {
return "CREATE INDEX webauthn_user_challenges_expires_at_index ON " +
Config.getConfig(start).getWebAuthNGeneratedOptionsTable() +
" (app_id, tenant_id, expires_at);";
}
public static String getQueryToCreateWebAuthNCredentialsTable(Start start){
return "CREATE TABLE IF NOT EXISTS "+ Config.getConfig(start).getWebAuthNCredentialsTable() + "(" +
" id VARCHAR(256) NOT NULL," +
" app_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
" rp_id VARCHAR(256)," +
" user_id CHAR(36)," +
" counter BIGINT NOT NULL," +
" public_key BLOB NOT NULL," + //planned as bytea, which is not supported by sqlite
" transports TEXT NOT NULL," + // planned as TEXT[], which is not supported by sqlite
" created_at BIGINT NOT NULL," +
" updated_at BIGINT NOT NULL," +
" CONSTRAINT webauthn_user_credentials_pkey PRIMARY KEY (app_id, rp_id, id)," +
" CONSTRAINT webauthn_user_credentials_webauthn_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES " +
Config.getConfig(start).getWebAuthNUsersTable() + " (app_id, user_id) ON DELETE CASCADE" +
");";
}
public static String getQueryToCreateWebAuthNCredentialsUserIdIndex(Start start) {
return "CREATE INDEX IF NOT EXISTS webauthn_credentials_user_id_index ON " +
Config.getConfig(start).getWebAuthNCredentialsTable() +
" (user_id);";
}
public static String getQueryToCreateWebAuthNAccountRecoveryTokenTable(Start start) {
return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() + "(" +
" app_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
" tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
" user_id CHAR(36) NOT NULL," +
" email VARCHAR(256) NOT NULL," +
" token VARCHAR(256) NOT NULL," +
" expires_at BIGINT UNSIGNED NOT NULL," +
" CONSTRAINT webauthn_account_recovery_token_pkey PRIMARY KEY (app_id, tenant_id, user_id, token)," +
" CONSTRAINT webauthn_account_recovery_token_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES " +
Config.getConfig(start).getUsersTable() + " (app_id, tenant_id, user_id) ON DELETE CASCADE" +
");";
}
public static String getQueryToCreateWebAuthNAccountRecoveryTokenTokenIndex(Start start) {
return "CREATE INDEX webauthn_account_recovery_token_token_index ON " +
Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() +
" (app_id, tenant_id, token);";
}
public static String getQueryToCreateWebAuthNAccountRecoveryTokenEmailIndex(Start start) {
return "CREATE INDEX webauthn_account_recovery_token_email_index ON " +
Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() +
" (app_id, tenant_id, email);";
}
public static String getQueryToCreateWebAuthNAccountRecoveryTokenExpiresAtIndex(Start start) {
return "CREATE INDEX webauthn_account_recovery_token_expires_at_index ON " +
Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() +
" (expires_at DESC);";
}
public static WebAuthNOptions saveOptions(Start start, TenantIdentifier tenantIdentifier, WebAuthNOptions options)
throws SQLException, StorageQueryException {
String INSERT = "INSERT INTO " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable()
+ " (app_id, tenant_id, id, challenge, email, rp_id, origin, expires_at, created_at, rp_name, user_verification, user_presence_required) "
+ " VALUES (?,?,?,?,?,?,?,?,?,?,?,?);";
update(start, INSERT, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, options.generatedOptionsId);
pst.setString(4, options.challenge);
pst.setString(5, options.userEmail);
pst.setString(6, options.relyingPartyId);
pst.setString(7, options.origin);
pst.setLong(8, options.expiresAt);
pst.setLong(9, options.createdAt);
pst.setString(10, options.relyingPartyName);
pst.setString(11, options.userVerification);
pst.setBoolean(12, options.userPresenceRequired);
});
return options;
}
public static WebAuthNOptions loadOptionsById(Start start, TenantIdentifier tenantIdentifier, String optionsId)
throws SQLException, StorageQueryException {
String QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable()
+ " WHERE app_id = ? AND tenant_id = ? and id = ?";
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, optionsId);
}, result -> {
if(result.next()){
return WebAuthNOptionsRowMapper.getInstance().mapOrThrow(result); // we are expecting one or zero results
}
return null;
});
}
public static WebAuthNStoredCredential loadCredentialByIdForUser(Start start, TenantIdentifier tenantIdentifier, String credentialId, String recipeUserId)
throws StorageQueryException, SQLException {
String QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNCredentialsTable()
+ " WHERE app_id = ? AND id = ? AND user_id = ?";
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, credentialId);
pst.setString(3, recipeUserId);
}, result -> {
if(result.next()){
return WebAuthnStoredCredentialRowMapper.getInstance().mapOrThrow(result); // we are expecting one or zero results
}
return null;
});
}
public static WebAuthNStoredCredential loadCredentialById_Transaction(Start start, Connection sqlConnection, TenantIdentifier tenantIdentifier, String credentialId)
throws SQLException, StorageQueryException {
String QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNCredentialsTable()
+ " WHERE app_id = ? AND id = ?";
return execute(sqlConnection, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, credentialId);
}, result -> {
if(result.next()){
return WebAuthnStoredCredentialRowMapper.getInstance().mapOrThrow(result); // we are expecting one or zero results
}
return null;
});
}
public static WebAuthNStoredCredential saveCredential(Start start, TenantIdentifier tenantIdentifier, WebAuthNStoredCredential credential)
throws SQLException, StorageQueryException {
String INSERT = "INSERT INTO " + Config.getConfig(start).getWebAuthNCredentialsTable()
+ " (id, app_id, rp_id, user_id, counter, public_key, transports, created_at, updated_at) "
+ " VALUES (?,?,?,?,?,?,?,?,?);";
update(start, INSERT, pst -> {
pst.setString(1, credential.id);
pst.setString(2, credential.appId);
pst.setString(3, credential.rpId);
pst.setString(4, credential.userId);
pst.setLong(5, credential.counter);
pst.setBytes(6, credential.publicKey);
pst.setString(7, credential.transports);
pst.setLong(8, credential.createdAt);
pst.setLong(9, credential.updatedAt);
});
return credential;
}
public static WebAuthNStoredCredential saveCredential_Transaction(Start start, Connection connection, TenantIdentifier tenantIdentifier, WebAuthNStoredCredential credential)
throws SQLException, StorageQueryException {
String INSERT = "INSERT INTO " + Config.getConfig(start).getWebAuthNCredentialsTable()
+ " (id, app_id, rp_id, user_id, counter, public_key, transports, created_at, updated_at) "
+ " VALUES (?,?,?,?,?,?,?,?,?);";
update(connection, INSERT, pst -> {
pst.setString(1, credential.id);
pst.setString(2, credential.appId);
pst.setString(3, credential.rpId);
pst.setString(4, credential.userId);
pst.setLong(5, credential.counter);
pst.setBytes(6, credential.publicKey);
pst.setString(7, credential.transports);
pst.setLong(8, credential.createdAt);
pst.setLong(9, credential.updatedAt);
});
return credential;
}
public static void createUser_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId, String email,
String relyingPartyId)
throws StorageTransactionLogicException, StorageQueryException {
long timeJoined = System.currentTimeMillis();
try {
// app_id_to_user_id
String insertAppIdToUserId = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable()
+ "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)";
update(sqlCon, insertAppIdToUserId, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, userId);
pst.setString(3, userId);
pst.setString(4, WEBAUTHN.toString());
});
// all_auth_recipe_users
String insertAllAuthRecipeUsers = "INSERT INTO " + getConfig(start).getUsersTable()
+
"(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, " +
"primary_or_recipe_user_time_joined)" +
" VALUES(?, ?, ?, ?, ?, ?, ?)";
update(sqlCon, insertAllAuthRecipeUsers, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, userId);
pst.setString(4, userId);
pst.setString(5, WEBAUTHN.toString());
pst.setLong(6, timeJoined);
pst.setLong(7, timeJoined);
});
// webauthn_user_to_tenant
String insertWebauthNUsersToTenant =
"INSERT INTO " + Config.getConfig(start).getWebAuthNUserToTenantTable()
+ " (app_id, tenant_id, user_id, email) "
+ " VALUES (?,?,?,?);";
update(sqlCon, insertWebauthNUsersToTenant, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, userId);
pst.setString(4, email);
});
// webauthn_users
String insertWebauthNUsers = "INSERT INTO " + Config.getConfig(start).getWebAuthNUsersTable()
+ " (app_id, user_id, email, rp_id, time_joined) "
+ " VALUES (?,?,?,?,?);";
update(sqlCon, insertWebauthNUsers, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, userId);
pst.setString(3, email);
pst.setString(4, relyingPartyId);
pst.setLong(5, timeJoined);
});
} catch (SQLException throwables) {
throw new StorageTransactionLogicException(throwables);
}
}
public static AuthRecipeUserInfo signUp_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId, String email,
String relyingPartyId)
throws StorageTransactionLogicException, StorageQueryException, SQLException {
createUser_Transaction(start, sqlCon, tenantIdentifier, userId, email, relyingPartyId);
return getAuthRecipeUserInfo(start, sqlCon,
tenantIdentifier, userId);
}
public static AuthRecipeUserInfo signUpWithCredentialRegister_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId, String email,
String relyingPartyId, WebAuthNStoredCredential credential)
throws StorageQueryException, StorageTransactionLogicException, SQLException {
createUser_Transaction(start, sqlCon, tenantIdentifier, userId, email, relyingPartyId);
saveCredential_Transaction(start, sqlCon, tenantIdentifier, credential);
return getAuthRecipeUserInfo(start, sqlCon,
tenantIdentifier, userId);
}
@Nullable
private static AuthRecipeUserInfo getAuthRecipeUserInfo(Start start, Connection sqlCon,
TenantIdentifier tenantIdentifier, String userId)
throws SQLException, StorageQueryException {
Collection<? extends LoginMethod> loginMethods = getUsersInfoUsingIdList_Transaction(start, sqlCon,
Collections.singleton(userId), tenantIdentifier.toAppIdentifier());
AuthRecipeUserInfo userInfo = null;
if (!loginMethods.isEmpty()) {
for (LoginMethod loginMethod : loginMethods) {
if(userInfo == null) {
userInfo = AuthRecipeUserInfo.create(userId, false, loginMethod);
} else {
userInfo.addLoginMethod(loginMethod);
if(!loginMethod.getSupertokensUserId().equals(loginMethod.getSupertokensOrExternalUserId())){
userInfo.setExternalUserId(loginMethod.getSupertokensOrExternalUserId());
}
}
}
}
return userInfo;
}
public static String getPrimaryUserIdForTenantUsingEmail(Start start, TenantIdentifier tenantIdentifier,
String email)
throws StorageQueryException {
try {
return start.startTransaction(con -> {
try {
Connection sqlConnection = (Connection) con.getConnection();
return getPrimaryUserIdForTenantUsingEmail_Transaction(start, sqlConnection, tenantIdentifier,
email);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
});
} catch (StorageTransactionLogicException e) {
throw new StorageQueryException(e);
}
}
public static String getPrimaryUserIdForTenantUsingEmail_Transaction(Start start, Connection sqlConnection,
TenantIdentifier tenantIdentifier,
String email)
throws SQLException, StorageQueryException {
String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ "FROM " + getConfig(start).getWebAuthNUserToTenantTable() + " AS ep" +
" JOIN " + getConfig(start).getUsersTable() + " AS all_users" +
" ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" +
" WHERE ep.app_id = ? AND ep.email = ? AND ep.tenant_id = ?";
return execute(sqlConnection, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, email);
pst.setString(3, tenantIdentifier.getTenantId());
}, result -> {
if (result.next()) {
return result.getString("user_id");
}
return null;
});
}
public static String getPrimaryUserIdForAppUsingEmail_Transaction(Start start, Connection sqlConnection,
AppIdentifier appIdentifier, String email)
throws SQLException, StorageQueryException {
String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
+ "FROM " + getConfig(start).getWebAuthNUserToTenantTable() + " AS ep" +
" JOIN " + getConfig(start).getUsersTable() + " AS all_users" +
" ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" +
" WHERE ep.app_id = ? AND ep.email = ?";
return execute(sqlConnection, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, email);
}, result -> {
if (result.next()) {
return result.getString("user_id");
}
return null;
});
}
public static Collection<? extends LoginMethod> getUsersInfoUsingIdList(Start start, Set<String> ids, AppIdentifier appIdentifier)
throws StorageQueryException {
try {
return start.startTransaction(con -> {
Connection sqlConnection = (Connection) con.getConnection();
try {
return getUsersInfoUsingIdList_Transaction(start, sqlConnection, ids, appIdentifier);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
});
} catch (StorageTransactionLogicException e) {
throw new StorageQueryException(e);
}
}
public static Collection<? extends LoginMethod> getUsersInfoUsingIdList_Transaction(Start start, Connection connection, Set<String> ids, AppIdentifier appIdentifier)
throws SQLException, StorageQueryException {
if (!ids.isEmpty()) {
String webauthnUsersTable = getConfig(start).getWebAuthNUsersTable();
String credentialTable = getConfig(start).getWebAuthNCredentialsTable();
String usersTable = getConfig(start).getUsersTable();
String userIdMappingTable = getConfig(start).getUserIdMappingTable();
String emailVerificationTable = getConfig(start).getEmailVerificationTable();
String queryAll = "SELECT webauthn.user_id as user_id, webauthn.email as email, webauthn.time_joined as time_joined, " +
"credentials.id as credential_id, email_verification.email as email_verified, user_id_mapping.external_user_id as external_user_id," +
"all_users.tenant_id as tenant_id " +
"FROM " + webauthnUsersTable + " as webauthn " +
"JOIN " + usersTable + " as all_users ON webauthn.app_id = all_users.app_id AND webauthn.user_id = all_users.user_id " +
"LEFT JOIN " + credentialTable + " as credentials ON webauthn.user_id = credentials.user_id " +
"LEFT JOIN " + userIdMappingTable + " as user_id_mapping ON webauthn.user_id = user_id_mapping.supertokens_user_id " +
"LEFT JOIN " + emailVerificationTable + " as email_verification ON webauthn.app_id = email_verification.app_id AND (user_id_mapping.external_user_id = email_verification.user_id OR user_id_mapping.supertokens_user_id = email_verification.user_id OR webauthn.user_id = email_verification.user_id) " +
" AND email_verification.email = webauthn.email " +
"WHERE webauthn.app_id = ? AND webauthn.user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ")";
return execute(connection, queryAll, pst -> {
pst.setString(1, appIdentifier.getAppId());
int index = 2;
for (String id : ids) {
pst.setString(index++, id);
}
}, result -> {
Map<String, LoginMethod> users = new HashMap<>();
while (result.next()) {
String userId = result.getString("user_id");
String email = result.getString("email");
long timeJoined = result.getLong("time_joined");
String credentialId = result.getString("credential_id");
boolean emailVerified = result.getString("email_verified") != null;
String externalUserId = result.getString("external_user_id");
String tenantId = result.getString("tenant_id");
if(users.containsKey(userId)) {
users.get(userId).webauthN.addCredentialId(credentialId);
users.get(userId).tenantIds.add(tenantId);
} else {
List<String> credentialIds = new ArrayList<>();
credentialIds.add(credentialId);
LoginMethod loginMethod = new LoginMethod(userId, timeJoined, emailVerified, email, new LoginMethod.WebAuthN(credentialIds), new String[]{tenantId});
loginMethod.setExternalUserId(externalUserId);
users.put(userId, loginMethod);
}
}
return users.values();
});
}
return Collections.emptyList();
}
public static AuthRecipeUserInfo getUserInfoByCredentialId_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String credentialId)
throws SQLException, StorageQueryException {
String QUERY = "SELECT webauthn.user_id as user_id, webauthn.email as email, webauthn.time_joined as time_joined, " +
"credentials.id as credential_id, email_verification.email as email_verified, user_id_mapping.external_user_id as external_user_id," +
"all_users.tenant_id as tenant_id " +
"FROM " + getConfig(start).getWebAuthNUsersTable() + " as webauthn " +
"JOIN " + getConfig(start).getUsersTable() + " as all_users ON webauthn.app_id = all_users.app_id AND webauthn.user_id = all_users.user_id " +
"LEFT JOIN " + getConfig(start).getWebAuthNCredentialsTable() + " as credentials ON webauthn.user_id = credentials.user_id " +
"LEFT JOIN " + getConfig(start).getUserIdMappingTable() + " as user_id_mapping ON webauthn.user_id = user_id_mapping.supertokens_user_id " +
"LEFT JOIN " + getConfig(start).getEmailVerificationTable() + " as email_verification ON webauthn.app_id = email_verification.app_id AND (user_id_mapping.external_user_id = email_verification.user_id OR user_id_mapping.supertokens_user_id = email_verification.user_id OR webauthn.user_id = email_verification.user_id)" +
" AND email_verification.email = webauthn.email " +
"WHERE webauthn.app_id = ? AND credentials.id = ?";
return execute(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, credentialId);
}, result -> {
if (result.next()) {
String userId = result.getString("user_id");
String email = result.getString("email");
long timeJoined = result.getLong("time_joined");
boolean emailVerified = result.getString("email_verified") != null;
String externalUserId = result.getString("external_user_id");
String tenantId = result.getString("tenant_id");
List<String> credentialIds = new ArrayList<>();
credentialIds.add(credentialId);
LoginMethod.WebAuthN webAuthNLM = new LoginMethod.WebAuthN(credentialIds);
LoginMethod loginMethod = new LoginMethod(userId, timeJoined, emailVerified, email, webAuthNLM, new String[]{tenantId});
if(externalUserId != null) {
loginMethod.setExternalUserId(externalUserId);
}
AuthRecipeUserInfo resultUserInfo = AuthRecipeUserInfo.create(userId, false, loginMethod);
resultUserInfo.setExternalUserId(externalUserId);
return resultUserInfo;
}
return null;
});
}
public static WebAuthNOptions loadOptionsById_Transaction(Start start, Connection sqlCon,
TenantIdentifier tenantIdentifier, String optionsId)
throws SQLException, StorageQueryException {
String QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable()
+ " WHERE app_id = ? AND id = ?";
return execute(sqlCon, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, optionsId);
}, result -> {
if(result.next()){
return WebAuthNOptionsRowMapper.getInstance().mapOrThrow(result); // we are expecting one or zero results
}
return null;
});
}
public static void updateCounter_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String credentialId, long counter)
throws SQLException, StorageQueryException {
String UPDATE = "UPDATE " + Config.getConfig(start).getWebAuthNCredentialsTable()
+ " SET counter = ?, updated_at = ? WHERE app_id = ? AND id = ?";
update(sqlCon, UPDATE, pst -> {
pst.setLong(1, counter);
pst.setLong(2, System.currentTimeMillis());
pst.setString(3, tenantIdentifier.getAppId());
pst.setString(4, credentialId);
});
}
public static int removeCredential(Start start, TenantIdentifier tenantIdentifier, String userId, String credentialId)
throws SQLException, StorageQueryException {
String UPDATE = "DELETE FROM " + Config.getConfig(start).getWebAuthNCredentialsTable()
+ " WHERE app_id = ? AND id = ? AND user_id = ?";
return update(start, UPDATE, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, credentialId);
pst.setString(3, userId);
});
}
public static int removeOptions(Start start, TenantIdentifier tenantIdentifier, String optionsId)
throws SQLException, StorageQueryException {
String UPDATE = "DELETE FROM " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable()
+ " WHERE app_id = ? AND tenant_id = ? AND id = ?";
return update(start, UPDATE, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, optionsId);
});
}
public static List<WebAuthNStoredCredential> listCredentials(Start start, TenantIdentifier tenantIdentifier,
String recipeUserId) throws SQLException, StorageQueryException {
String LIST_QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNCredentialsTable() +
" WHERE app_id = ? AND user_id = ?";
return execute(start, LIST_QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, recipeUserId);
}, result -> {
List<WebAuthNStoredCredential> credentials = new ArrayList<>();
while (result.next()) {
credentials.add(WebAuthnStoredCredentialRowMapper.getInstance().mapOrThrow(result));
}
return credentials;
});
}
public static void updateUserEmail(Start start, TenantIdentifier tenantIdentifier, String userId, String newEmail)
throws StorageQueryException {
try {
start.startTransaction(con -> {
updateUserEmail_Transaction(start, (Connection) con.getConnection(), tenantIdentifier, userId, newEmail);
return null;
});
} catch (StorageTransactionLogicException e) {
throw new StorageQueryException(e);
}
}
public static void updateUserEmail_Transaction(Start start, Connection sqlConnection, TenantIdentifier tenantIdentifier,
String userId, String newEmail) throws StorageQueryException {
try {
String UPDATE_USER_TO_TENANT_QUERY =
"UPDATE " + Config.getConfig(start).getWebAuthNUserToTenantTable() +
" SET email = ? WHERE app_id = ? AND tenant_id = ? AND user_id = ?";
String UPDATE_USER_QUERY = "UPDATE " + Config.getConfig(start).getWebAuthNUsersTable() +
" SET email = ? WHERE app_id = ? AND user_id = ?";
update(sqlConnection, UPDATE_USER_TO_TENANT_QUERY, pst -> {
pst.setString(1, newEmail);
pst.setString(2, tenantIdentifier.getAppId());
pst.setString(3, tenantIdentifier.getTenantId());
pst.setString(4, userId);
});
update(sqlConnection, UPDATE_USER_QUERY, pst -> {
pst.setString(1, newEmail);
pst.setString(2, tenantIdentifier.getAppId());
pst.setString(3, userId);
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
private static class WebAuthnStoredCredentialRowMapper implements RowMapper<WebAuthNStoredCredential, ResultSet> {
private static final WebAuthnStoredCredentialRowMapper INSTANCE = new WebAuthnStoredCredentialRowMapper();
public static WebAuthnStoredCredentialRowMapper getInstance() {
return INSTANCE;
}
@Override
public WebAuthNStoredCredential map(ResultSet rs) throws Exception {
WebAuthNStoredCredential result = new WebAuthNStoredCredential();
result.id = rs.getString("id");
result.appId = rs.getString("app_id");
result.rpId = rs.getString("rp_id");
result.userId = rs.getString("user_id");
result.counter = rs.getLong("counter");
result.publicKey = rs.getBytes("public_key");
result.transports = rs.getString("transports");
result.createdAt = rs.getLong("created_at");
result.updatedAt = rs.getLong("updated_at");
return result;
}
}
private static class WebAuthNOptionsRowMapper implements RowMapper<WebAuthNOptions, ResultSet> {
private static final WebAuthNOptionsRowMapper INSTANCE = new WebAuthNOptionsRowMapper();
public static WebAuthNOptionsRowMapper getInstance() {
return INSTANCE;
}
@Override
public WebAuthNOptions map(ResultSet rs) throws Exception {
WebAuthNOptions result = new WebAuthNOptions();
result.timeout = rs.getLong("expires_at") - rs.getLong("created_at");
result.expiresAt = rs.getLong("expires_at");
result.createdAt = rs.getLong("created_at");
result.relyingPartyId = rs.getString("rp_id");
result.origin = rs.getString("origin");
result.challenge = rs.getString("challenge");
result.userEmail = rs.getString("email");
result.generatedOptionsId = rs.getString("id");
result.relyingPartyName = rs.getString("rp_name");
result.userPresenceRequired = rs.getBoolean("user_presence_required");
result.userVerification = rs.getString("user_verification");
return result;
}
}
public static void addRecoverAccountToken(Start start, TenantIdentifier tenantIdentifier, AccountRecoveryTokenInfo accountRecoveryTokenInfo)
throws SQLException, StorageQueryException {
String INSERT = "INSERT INTO " + Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() + " (app_id, tenant_id, user_id, email, token, expires_at) VALUES (?, ?, ?, ?, ?, ?)";
update(start, INSERT, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, accountRecoveryTokenInfo.userId);
pst.setString(4, accountRecoveryTokenInfo.email);
pst.setString(5, accountRecoveryTokenInfo.token);
pst.setLong(6, accountRecoveryTokenInfo.expiresAt);
});
}
public static AccountRecoveryTokenInfo getAccountRecoveryTokenInfoByToken_Transaction(Start start, TenantIdentifier tenantIdentifier, Connection con, String token)
throws SQLException, StorageQueryException {
String QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() + " WHERE app_id = ? AND tenant_id = ? AND token = ?";
return execute(con, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, token);
}, result -> {
if (result.next()) {
return AccountRecoveryTokenInfoRowMapper.getInstance().mapOrThrow(result);
}
return null;
});
}
private static class AccountRecoveryTokenInfoRowMapper implements RowMapper<AccountRecoveryTokenInfo, ResultSet> {
private static final AccountRecoveryTokenInfoRowMapper INSTANCE = new AccountRecoveryTokenInfoRowMapper();
public static AccountRecoveryTokenInfoRowMapper getInstance() {
return INSTANCE;
}
@Override
public AccountRecoveryTokenInfo map(ResultSet rs) throws Exception {
AccountRecoveryTokenInfo result = new AccountRecoveryTokenInfo(
rs.getString("user_id"),
rs.getString("email"),
rs.getString("token"),
rs.getLong("expires_at")
);
return result;
}
}
public static void deleteAccountRecoveryTokenByEmail_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String email)
throws SQLException, StorageQueryException {
String DELETE = "DELETE FROM " + Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() + " WHERE app_id = ? AND tenant_id = ? AND email = ?";
update(con, DELETE, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, email);
});
}
public static void deleteExpiredAccountRecoveryTokens(Start start)
throws SQLException, StorageQueryException {
String DELETE = "DELETE FROM " + Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() + " WHERE expires_at < ?";
update(start, DELETE, pst -> {
pst.setLong(1, System.currentTimeMillis());
});
}
public static void deleteExpiredGeneratedOptions(Start start)
throws SQLException, StorageQueryException {
String DELETE = "DELETE FROM " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable() + " WHERE expires_at < ?";
update(start, DELETE, pst -> {
pst.setLong(1, System.currentTimeMillis());
});
}
}

View File

@ -23,6 +23,7 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import io.supertokens.Main;
import io.supertokens.ResourceDistributor;
import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
@ -54,7 +55,7 @@ public class JWTSigningFunctions {
throws StorageQueryException, StorageTransactionLogicException, NoSuchAlgorithmException,
InvalidKeySpecException, JWTCreationException, UnsupportedJWTSigningAlgorithmException {
try {
return createJWTToken(new AppIdentifier(null, null), main, algorithm, payload, jwksDomain,
return createJWTToken(ResourceDistributor.getAppForTesting().toAppIdentifier(), main, algorithm, payload, jwksDomain,
jwtValidityInSeconds, useDynamicKey);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);

View File

@ -33,6 +33,8 @@ import io.supertokens.pluginInterface.exceptions.InvalidConfigException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.*;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.saml.SAMLCertificate;
import io.supertokens.pluginInterface.opentelemetry.WithinOtelSpan;
import io.supertokens.session.refreshToken.RefreshTokenKey;
import io.supertokens.signingkeys.AccessTokenSigningKey;
import io.supertokens.signingkeys.JWTSigningKey;
@ -116,6 +118,7 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
return StorageLayer.getMultitenancyStorage(main).getAllTenants();
}
@WithinOtelSpan
public List<TenantIdentifier> refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(
boolean reloadAllResources) {
try {
@ -233,6 +236,7 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
}
AccessTokenSigningKey.loadForAllTenants(main, apps, tenantsThatChanged);
RefreshTokenKey.loadForAllTenants(main, apps, tenantsThatChanged);
SAMLCertificate.loadForAllTenants(main, apps, tenantsThatChanged);
JWTSigningKey.loadForAllTenants(main, apps, tenantsThatChanged);
SigningKeys.loadForAllTenants(main, apps, tenantsThatChanged);
}

View File

@ -22,8 +22,8 @@ public class HttpRequestForOAuthProvider {
// case of errors, etc.
// Left the original HttpRequest as is to avoid any issues with existing code.
private static final int CONNECTION_TIMEOUT = 5000;
private static final int READ_TIMEOUT = 5000;
private static final int CONNECTION_TIMEOUT = 15000;
private static final int READ_TIMEOUT = 15000;
public static Response doGet(String url, Map<String, String> headers, Map<String, String> queryParams) throws IOException {
if (queryParams == null) {

View File

@ -16,6 +16,7 @@
package io.supertokens.output;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
@ -32,6 +33,7 @@ import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.telemetry.TelemetryProvider;
import io.supertokens.utils.Utils;
import io.supertokens.version.Version;
import io.supertokens.webserver.Webserver;
@ -54,6 +56,12 @@ public class Logging extends ResourceDistributor.SingletonResource {
public static final String ANSI_WHITE = "\u001B[37m";
private Logging(Main main) {
// Set global logging level
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
Level newLevel = Level.toLevel(Config.getBaseConfig(main).getLogLevel(), Level.INFO); // Default to INFO if invalid
rootLogger.setLevel(newLevel);
this.infoLogger = Config.getBaseConfig(main).getInfoLogPath(main).equals("null")
? createLoggerForConsole(main, "io.supertokens.Info", LOG_LEVEL.INFO)
: createLoggerForFile(main, Config.getBaseConfig(main).getInfoLogPath(main),
@ -65,7 +73,7 @@ public class Logging extends ResourceDistributor.SingletonResource {
Storage storage = StorageLayer.getBaseStorage(main);
if (storage != null) {
storage.initFileLogging(Config.getBaseConfig(main).getInfoLogPath(main),
Config.getBaseConfig(main).getErrorLogPath(main));
Config.getBaseConfig(main).getErrorLogPath(main), TelemetryProvider.getInstance(main));
}
try {
// we wait here for a bit so that the loggers can be properly initialised..
@ -111,7 +119,9 @@ public class Logging extends ResourceDistributor.SingletonResource {
try {
msg = msg.trim();
if (getInstance(main) != null) {
getInstance(main).infoLogger.debug(getFormattedMessage(tenantIdentifier, msg));
String formattedMsg = getFormattedMessage(tenantIdentifier, msg);
getInstance(main).infoLogger.debug(formattedMsg);
TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, formattedMsg, "debug");
}
} catch (NullPointerException e) {
// sometimes logger.debug throws a null pointer exception...
@ -162,6 +172,8 @@ public class Logging extends ResourceDistributor.SingletonResource {
if (getInstance(main) != null) {
getInstance(main).infoLogger.info(msg);
}
TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, msg, "info");
} catch (NullPointerException ignored) {
}
}
@ -175,6 +187,7 @@ public class Logging extends ResourceDistributor.SingletonResource {
msg = getFormattedMessage(tenantIdentifier, msg);
if (getInstance(main) != null) {
getInstance(main).errorLogger.warn(msg);
TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, msg, "warn");
}
} catch (NullPointerException ignored) {
}
@ -194,7 +207,9 @@ public class Logging extends ResourceDistributor.SingletonResource {
try {
err = err.trim();
if (getInstance(main) != null) {
getInstance(main).errorLogger.error(getFormattedMessage(tenantIdentifier, err));
String formattedMessage = getFormattedMessage(tenantIdentifier, err);
getInstance(main).errorLogger.error(formattedMessage);
TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, formattedMessage, "error");
}
if (toConsoleAsWell || getInstance(main) == null) {
systemErr(prependTenantIdentifierToMessage(tenantIdentifier, err));
@ -228,6 +243,9 @@ public class Logging extends ResourceDistributor.SingletonResource {
message = message.trim();
if (getInstance(main) != null) {
getInstance(main).errorLogger.error(getFormattedMessage(tenantIdentifier, message, e));
TelemetryProvider.getInstance(main)
.createLogEvent(tenantIdentifier, getFormattedMessage(tenantIdentifier, message, e),
"error");
}
if (toConsoleAsWell || getInstance(main) == null) {
systemErr(prependTenantIdentifierToMessage(tenantIdentifier, message));

View File

@ -17,6 +17,7 @@
package io.supertokens.passwordless;
import io.supertokens.Main;
import io.supertokens.ResourceDistributor;
import io.supertokens.authRecipe.AuthRecipe;
import io.supertokens.config.Config;
import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
@ -74,7 +75,7 @@ public class Passwordless {
try {
Storage storage = StorageLayer.getStorage(main);
return createCode(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
main, email, phoneNumber, deviceId, userInputCode);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -161,7 +162,7 @@ public class Passwordless {
NoSuchAlgorithmException, Base64EncodingException {
Storage storage = StorageLayer.getStorage(main);
return getDeviceWithCodesById(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
deviceId);
}
@ -170,7 +171,7 @@ public class Passwordless {
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getDeviceWithCodesByIdHash(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
deviceIdHash);
}
@ -210,7 +211,7 @@ public class Passwordless {
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getDevicesWithCodesByEmail(
new TenantIdentifier(null, null, null), storage, email);
ResourceDistributor.getAppForTesting(), storage, email);
}
public static List<DeviceWithCodes> getDevicesWithCodesByPhoneNumber(
@ -235,7 +236,7 @@ public class Passwordless {
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getDevicesWithCodesByPhoneNumber(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
phoneNumber);
}
@ -249,7 +250,7 @@ public class Passwordless {
try {
Storage storage = StorageLayer.getStorage(main);
return consumeCode(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, false);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -266,7 +267,7 @@ public class Passwordless {
try {
Storage storage = StorageLayer.getStorage(main);
return consumeCode(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, setEmailVerified);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -567,7 +568,7 @@ public class Passwordless {
public static void removeCode(Main main, String codeId)
throws StorageQueryException, StorageTransactionLogicException {
Storage storage = StorageLayer.getStorage(main);
removeCode(new TenantIdentifier(null, null, null), storage,
removeCode(ResourceDistributor.getAppForTesting(), storage,
codeId);
}
@ -622,7 +623,7 @@ public class Passwordless {
throws StorageQueryException, StorageTransactionLogicException {
Storage storage = StorageLayer.getStorage(main);
removeCodesByEmail(
new TenantIdentifier(null, null, null), storage, email);
ResourceDistributor.getAppForTesting(), storage, email);
}
public static void removeCodesByEmail(TenantIdentifier tenantIdentifier, Storage storage, String email)
@ -642,7 +643,7 @@ public class Passwordless {
throws StorageQueryException, StorageTransactionLogicException {
Storage storage = StorageLayer.getStorage(main);
removeCodesByPhoneNumber(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
phoneNumber);
}
@ -664,7 +665,7 @@ public class Passwordless {
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getUserById(
new AppIdentifier(null, null), storage, userId);
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId);
}
@Deprecated
@ -689,7 +690,7 @@ public class Passwordless {
String phoneNumber) throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getUserByPhoneNumber(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
phoneNumber);
}
@ -714,7 +715,7 @@ public class Passwordless {
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getUserByEmail(
new TenantIdentifier(null, null, null), storage, email);
ResourceDistributor.getAppForTesting(), storage, email);
}
@Deprecated
@ -740,7 +741,7 @@ public class Passwordless {
DuplicatePhoneNumberException, UserWithoutContactInfoException, EmailChangeNotAllowedException,
PhoneNumberChangeNotAllowedException {
Storage storage = StorageLayer.getStorage(main);
updateUser(new AppIdentifier(null, null), storage,
updateUser(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
userId, emailUpdate, phoneNumberUpdate);
}

View File

@ -0,0 +1,690 @@
/*
* Copyright (c) 2025, 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.saml;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.XMLObjectBuilderFactory;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.UnmarshallingException;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.saml.common.SAMLVersion;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.Audience;
import org.opensaml.saml.saml2.core.AudienceRestriction;
import org.opensaml.saml.saml2.core.AuthnContext;
import org.opensaml.saml.saml2.core.AuthnContextClassRef;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.Conditions;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.NameIDPolicy;
import org.opensaml.saml.saml2.core.RequestedAuthnContext;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialSupport;
import org.opensaml.xmlsec.signature.KeyInfo;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.X509Data;
import org.opensaml.xmlsec.signature.impl.KeyInfoBuilder;
import org.opensaml.xmlsec.signature.impl.SignatureBuilder;
import org.opensaml.xmlsec.signature.impl.X509DataBuilder;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.opensaml.xmlsec.signature.support.SignatureException;
import org.opensaml.xmlsec.signature.support.SignatureValidator;
import org.w3c.dom.Element;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.supertokens.Main;
import io.supertokens.config.Config;
import io.supertokens.config.CoreConfig;
import io.supertokens.featureflag.EE_FEATURES;
import io.supertokens.featureflag.FeatureFlag;
import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.StorageUtils;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.saml.SAMLClaimsInfo;
import io.supertokens.pluginInterface.saml.SAMLClient;
import io.supertokens.pluginInterface.saml.SAMLRelayStateInfo;
import io.supertokens.pluginInterface.saml.SAMLStorage;
import io.supertokens.pluginInterface.saml.exception.DuplicateEntityIdException;
import io.supertokens.saml.exceptions.IDPInitiatedLoginDisallowedException;
import io.supertokens.saml.exceptions.InvalidClientException;
import io.supertokens.saml.exceptions.InvalidCodeException;
import io.supertokens.saml.exceptions.InvalidRelayStateException;
import io.supertokens.saml.exceptions.MalformedSAMLMetadataXMLException;
import io.supertokens.saml.exceptions.SAMLResponseVerificationFailedException;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
import net.shibboleth.utilities.java.support.xml.XMLParserException;
public class SAML {
public static void checkForSAMLFeature(AppIdentifier appIdentifier, Main main)
throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException {
EE_FEATURES[] features = FeatureFlag.getInstance(main, appIdentifier).getEnabledFeatures();
for (EE_FEATURES f : features) {
if (f == EE_FEATURES.SAML) {
return;
}
}
throw new FeatureNotEnabledException(
"SAML feature is not enabled. Please subscribe to a SuperTokens core license key to enable this " +
"feature.");
}
public static SAMLClient createOrUpdateSAMLClient(
Main main, TenantIdentifier tenantIdentifier, Storage storage,
String clientId, String clientSecret, String defaultRedirectURI, JsonArray redirectURIs, String metadataXML, boolean allowIDPInitiatedLogin, boolean enableRequestSigning)
throws MalformedSAMLMetadataXMLException, StorageQueryException, CertificateException,
FeatureNotEnabledException, TenantOrAppNotFoundException, DuplicateEntityIdException {
checkForSAMLFeature(tenantIdentifier.toAppIdentifier(), main);
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
var metadata = loadIdpMetadata(metadataXML);
String idpSsoUrl = null;
for (var roleDescriptor : metadata.getRoleDescriptors()) {
if (roleDescriptor instanceof IDPSSODescriptor) {
IDPSSODescriptor idpDescriptor = (IDPSSODescriptor) roleDescriptor;
for (SingleSignOnService ssoService : idpDescriptor.getSingleSignOnServices()) {
if (SAMLConstants.SAML2_REDIRECT_BINDING_URI.equals(ssoService.getBinding())) {
idpSsoUrl = ssoService.getLocation();
}
}
}
}
if (idpSsoUrl == null) {
throw new MalformedSAMLMetadataXMLException();
}
String idpSigningCertificate = extractIdpSigningCertificate(metadata);
getCertificateFromString(idpSigningCertificate); // checking validity
String idpEntityId = metadata.getEntityID();
SAMLClient client = new SAMLClient(clientId, clientSecret, idpSsoUrl, redirectURIs, defaultRedirectURI, idpEntityId, idpSigningCertificate, allowIDPInitiatedLogin, enableRequestSigning);
return samlStorage.createOrUpdateSAMLClient(tenantIdentifier, client);
}
public static List<SAMLClient> getClients(TenantIdentifier tenantIdentifier, Storage storage) throws StorageQueryException {
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
return samlStorage.getSAMLClients(tenantIdentifier);
}
public static SAMLClient getClient(TenantIdentifier tenantIdentifier, Storage storage, String clientId) throws StorageQueryException {
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
return samlStorage.getSAMLClient(tenantIdentifier, clientId);
}
public static boolean removeSAMLClient(TenantIdentifier tenantIdentifier, Storage storage, String clientId) throws StorageQueryException {
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
return samlStorage.removeSAMLClient(tenantIdentifier, clientId);
}
private static String extractIdpSigningCertificate(EntityDescriptor idpMetadata) {
for (var roleDescriptor : idpMetadata.getRoleDescriptors()) {
if (roleDescriptor instanceof IDPSSODescriptor) {
IDPSSODescriptor idpDescriptor = (IDPSSODescriptor) roleDescriptor;
for (org.opensaml.saml.saml2.metadata.KeyDescriptor keyDescriptor : idpDescriptor.getKeyDescriptors()) {
if (keyDescriptor.getUse() == null ||
"SIGNING".equals(keyDescriptor.getUse().toString())) {
org.opensaml.xmlsec.signature.KeyInfo keyInfo = keyDescriptor.getKeyInfo();
if (keyInfo != null) {
for (org.opensaml.xmlsec.signature.X509Data x509Data : keyInfo.getX509Datas()) {
for (org.opensaml.xmlsec.signature.X509Certificate x509Cert : x509Data.getX509Certificates()) {
try {
String certString = x509Cert.getValue();
if (certString != null && !certString.trim().isEmpty()) {
certString = certString.replaceAll("\\s", "");
return certString;
}
} catch (Exception e) {
// Continue to next certificate if this one fails
continue;
}
}
}
}
}
}
}
}
return null;
}
public static String createRedirectURL(Main main, TenantIdentifier tenantIdentifier, Storage storage,
String clientId, String redirectURI, String state, String acsURL)
throws StorageQueryException, InvalidClientException, TenantOrAppNotFoundException,
CertificateEncodingException, FeatureNotEnabledException {
checkForSAMLFeature(tenantIdentifier.toAppIdentifier(), main);
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
CoreConfig config = Config.getConfig(tenantIdentifier, main);
SAMLClient client = samlStorage.getSAMLClient(tenantIdentifier, clientId);
if (client == null) {
throw new InvalidClientException();
}
boolean redirectURIOk = false;
for (JsonElement rUri : client.redirectURIs) {
if (rUri.getAsString().equals(redirectURI)) {
redirectURIOk = true;
break;
}
}
if (!redirectURIOk) {
throw new InvalidClientException();
}
String idpSsoUrl = client.ssoLoginURL;
AuthnRequest request = buildAuthnRequest(
main,
tenantIdentifier.toAppIdentifier(),
idpSsoUrl,
config.getSAMLSPEntityID(), acsURL,
client.enableRequestSigning);
String samlRequest = deflateAndBase64RedirectMessage(request);
String relayState = UUID.randomUUID().toString();
samlStorage.saveRelayStateInfo(tenantIdentifier, new SAMLRelayStateInfo(relayState, clientId, state, redirectURI), config.getSAMLRelayStateValidity());
return idpSsoUrl + "?SAMLRequest=" + samlRequest + "&RelayState=" + URLEncoder.encode(relayState, StandardCharsets.UTF_8);
}
public static EntityDescriptor loadIdpMetadata(String metadataXML) throws MalformedSAMLMetadataXMLException {
try {
byte[] bytes = metadataXML.getBytes(StandardCharsets.UTF_8);
try (InputStream inputStream = new java.io.ByteArrayInputStream(bytes)) {
XMLObject xmlObject = XMLObjectSupport.unmarshallFromInputStream(
XMLObjectProviderRegistrySupport.getParserPool(), inputStream);
if (xmlObject instanceof EntityDescriptor) {
return (EntityDescriptor) xmlObject;
} else {
throw new RuntimeException("Expected EntityDescriptor but got: " + xmlObject.getClass());
}
}
} catch (Exception e) {
throw new MalformedSAMLMetadataXMLException();
}
}
private static AuthnRequest buildAuthnRequest(Main main, AppIdentifier appIdentifier, String idpSsoUrl, String spEntityId, String acsUrl, boolean enableRequestSigning)
throws TenantOrAppNotFoundException, StorageQueryException, CertificateEncodingException {
XMLObjectBuilderFactory builders = XMLObjectProviderRegistrySupport.getBuilderFactory();
AuthnRequest authnRequest = (AuthnRequest) builders
.<AuthnRequest>getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME)
.buildObject(AuthnRequest.DEFAULT_ELEMENT_NAME);
authnRequest.setID("_" + UUID.randomUUID());
authnRequest.setIssueInstant(Instant.now());
authnRequest.setVersion(SAMLVersion.VERSION_20);
authnRequest.setDestination(idpSsoUrl);
authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
Issuer issuer = (Issuer) builders.getBuilder(Issuer.DEFAULT_ELEMENT_NAME)
.buildObject(Issuer.DEFAULT_ELEMENT_NAME);
issuer.setValue(spEntityId);
authnRequest.setIssuer(issuer);
NameIDPolicy nameIDPolicy = (NameIDPolicy) builders.getBuilder(NameIDPolicy.DEFAULT_ELEMENT_NAME)
.buildObject(NameIDPolicy.DEFAULT_ELEMENT_NAME);
nameIDPolicy.setAllowCreate(true);
authnRequest.setNameIDPolicy(nameIDPolicy);
RequestedAuthnContext rac = (RequestedAuthnContext) builders.getBuilder(RequestedAuthnContext.DEFAULT_ELEMENT_NAME)
.buildObject(RequestedAuthnContext.DEFAULT_ELEMENT_NAME);
rac.setComparison(org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration.EXACT);
AuthnContextClassRef classRef = (AuthnContextClassRef) builders.getBuilder(AuthnContextClassRef.DEFAULT_ELEMENT_NAME)
.buildObject(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
classRef.setURI(AuthnContext.PASSWORD_AUTHN_CTX);
rac.getAuthnContextClassRefs().add(classRef);
authnRequest.setRequestedAuthnContext(rac);
authnRequest.setAssertionConsumerServiceURL(acsUrl);
if (enableRequestSigning) {
Signature signature = new SignatureBuilder().buildObject();
signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
// Create KeyInfo
KeyInfo keyInfo = new KeyInfoBuilder().buildObject();
X509Data x509Data = new X509DataBuilder().buildObject();
org.opensaml.xmlsec.signature.X509Certificate x509CertElement = new org.opensaml.xmlsec.signature.impl.X509CertificateBuilder().buildObject();
X509Certificate spCertificate = SAMLCertificate.getInstance(appIdentifier, main).getCertificate();
String certString = java.util.Base64.getEncoder().encodeToString(spCertificate.getEncoded());
x509CertElement.setValue(certString);
x509Data.getX509Certificates().add(x509CertElement);
keyInfo.getX509Datas().add(x509Data);
signature.setKeyInfo(keyInfo);
authnRequest.setSignature(signature);
}
return authnRequest;
}
private static String deflateAndBase64RedirectMessage(XMLObject xmlObject) {
try {
String xml = toXmlString(xmlObject);
byte[] xmlBytes = xml.getBytes(StandardCharsets.UTF_8);
// DEFLATE compression as per SAML Redirect binding spec
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DeflaterOutputStream dos = new DeflaterOutputStream(baos, new Deflater(Deflater.DEFLATED, true));
dos.write(xmlBytes);
dos.close();
byte[] deflated = baos.toByteArray();
String base64 = java.util.Base64.getEncoder().encodeToString(deflated);
return URLEncoder.encode(base64, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("Failed to deflate SAML message", e);
}
}
private static String toXmlString(XMLObject xmlObject) {
try {
Element el = XMLObjectSupport.marshall(xmlObject);
return SerializeSupport.nodeToString(el);
} catch (Exception e) {
throw new RuntimeException("Failed to serialize XML", e);
}
}
private static Response parseSamlResponse(String samlResponseBase64)
throws IOException, XMLParserException, UnmarshallingException {
byte[] decoded = java.util.Base64.getDecoder().decode(samlResponseBase64);
String xml = new String(decoded, StandardCharsets.UTF_8);
try (InputStream inputStream = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) {
return (Response) XMLObjectSupport.unmarshallFromInputStream(
XMLObjectProviderRegistrySupport.getParserPool(), inputStream);
}
}
private static void verifySamlResponseSignature(Response samlResponse, X509Certificate idpCertificate)
throws SignatureException {
Signature responseSignature = samlResponse.getSignature();
if (responseSignature != null) {
Credential credential = CredentialSupport.getSimpleCredential(idpCertificate, null);
SignatureValidator.validate(responseSignature, credential);
return;
}
boolean foundSignedAssertion = false;
for (Assertion assertion : samlResponse.getAssertions()) {
Signature assertionSignature = assertion.getSignature();
if (assertionSignature != null) {
Credential credential = CredentialSupport.getSimpleCredential(idpCertificate, null);
SignatureValidator.validate(assertionSignature, credential);
foundSignedAssertion = true;
}
}
if (!foundSignedAssertion) {
throw new RuntimeException("Neither SAML Response nor any Assertion is signed");
}
}
private static void validateSamlResponseTimestamps(Response samlResponse) throws SAMLResponseVerificationFailedException {
Instant now = Instant.now();
// Validate response issue instant (should be recent)
if (samlResponse.getIssueInstant() != null) {
Instant responseTime = samlResponse.getIssueInstant();
// Allow 5 minutes clock skew
if (responseTime.isAfter(now.plusSeconds(300)) || responseTime.isBefore(now.minusSeconds(300))) {
throw new SAMLResponseVerificationFailedException();
}
}
// Validate assertion timestamps
for (Assertion assertion : samlResponse.getAssertions()) {
// Check NotBefore
if (assertion.getConditions() != null && assertion.getConditions().getNotBefore() != null) {
if (now.isBefore(assertion.getConditions().getNotBefore())) {
throw new SAMLResponseVerificationFailedException();
}
}
// Check NotOnOrAfter
if (assertion.getConditions() != null && assertion.getConditions().getNotOnOrAfter() != null) {
if (now.isAfter(assertion.getConditions().getNotOnOrAfter())) {
throw new SAMLResponseVerificationFailedException();
}
}
}
}
public static String handleCallback(Main main, TenantIdentifier tenantIdentifier, Storage storage, String samlResponse, String relayState)
throws StorageQueryException, XMLParserException, IOException, UnmarshallingException,
CertificateException, InvalidRelayStateException, SAMLResponseVerificationFailedException,
InvalidClientException, IDPInitiatedLoginDisallowedException, TenantOrAppNotFoundException,
FeatureNotEnabledException {
checkForSAMLFeature(tenantIdentifier.toAppIdentifier(), main);
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
CoreConfig config = Config.getConfig(tenantIdentifier, main);
SAMLClient client = null;
Response response = parseSamlResponse(samlResponse);
String state = null;
String redirectURI = null;
if (relayState != null && !relayState.isEmpty()) {
// sp initiated
var relayStateInfo = samlStorage.getRelayStateInfo(tenantIdentifier, relayState);
if (relayStateInfo == null) {
throw new InvalidRelayStateException();
}
String clientId = relayStateInfo.clientId;
client = samlStorage.getSAMLClient(tenantIdentifier, clientId);
state = relayStateInfo.state;
redirectURI = relayStateInfo.redirectURI;
} else {
// idp initiated
String idpEntityId = response.getIssuer().getValue();
client = samlStorage.getSAMLClientByIDPEntityId(tenantIdentifier, idpEntityId);
redirectURI = client.defaultRedirectURI;
if (!client.allowIDPInitiatedLogin) {
throw new IDPInitiatedLoginDisallowedException();
}
}
if (client == null) {
throw new InvalidClientException();
}
// SAML verification
X509Certificate idpSigningCertificate = getCertificateFromString(client.idpSigningCertificate);
try {
verifySamlResponseSignature(response, idpSigningCertificate);
} catch (SignatureException e) {
throw new SAMLResponseVerificationFailedException();
}
validateSamlResponseTimestamps(response);
validateSamlResponseAudience(response, config.getSAMLSPEntityID());
var claims = extractAllClaims(response);
String code = UUID.randomUUID().toString();
samlStorage.saveSAMLClaims(tenantIdentifier, client.clientId, code, claims, config.getSAMLClaimsValidity());
try {
java.net.URI uri = new java.net.URI(redirectURI);
String query = uri.getQuery();
StringBuilder newQuery = new StringBuilder();
if (query != null && !query.isEmpty()) {
newQuery.append(query).append("&");
}
newQuery.append("code=").append(java.net.URLEncoder.encode(code, java.nio.charset.StandardCharsets.UTF_8));
if (state != null) {
newQuery.append("&state=").append(java.net.URLEncoder.encode(state, java.nio.charset.StandardCharsets.UTF_8));
}
java.net.URI newUri = new java.net.URI(
uri.getScheme(),
uri.getAuthority(),
uri.getPath(),
newQuery.toString(),
uri.getFragment()
);
return newUri.toString();
} catch (URISyntaxException e) {
throw new IllegalStateException("should never happen", e);
}
}
private static void validateSamlResponseAudience(Response samlResponse, String expectedAudience)
throws SAMLResponseVerificationFailedException {
boolean audienceMatched = false;
for (Assertion assertion : samlResponse.getAssertions()) {
Conditions conditions = assertion.getConditions();
if (conditions == null) {
continue;
}
java.util.List<AudienceRestriction> restrictions = conditions.getAudienceRestrictions();
if (restrictions == null || restrictions.isEmpty()) {
continue;
}
for (AudienceRestriction ar : restrictions) {
java.util.List<Audience> audiences = ar.getAudiences();
if (audiences == null || audiences.isEmpty()) {
continue;
}
for (Audience aud : audiences) {
if (expectedAudience.equals(aud.getURI())) {
audienceMatched = true;
break;
}
}
if (audienceMatched) {
break;
}
}
if (audienceMatched) {
break;
}
}
if (!audienceMatched) {
throw new SAMLResponseVerificationFailedException();
}
}
private static JsonObject extractAllClaims(Response samlResponse) {
JsonObject claims = new JsonObject();
for (Assertion assertion : samlResponse.getAssertions()) {
// Extract NameID as a claim
Subject subject = assertion.getSubject();
if (subject != null && subject.getNameID() != null) {
String nameId = subject.getNameID().getValue();
String nameIdFormat = subject.getNameID().getFormat();
JsonArray nameIdArr = new JsonArray();
nameIdArr.add(nameId);
claims.add("NameID", nameIdArr);
if (nameIdFormat != null) {
JsonArray nameIdFormatArr = new JsonArray();
nameIdFormatArr.add(nameIdFormat);
claims.add("NameIDFormat", nameIdFormatArr);
}
}
// Extract all attributes from AttributeStatements
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
for (Attribute attribute : attributeStatement.getAttributes()) {
String attributeName = attribute.getName();
JsonArray attributeValues = new JsonArray();
for (XMLObject attributeValue : attribute.getAttributeValues()) {
if (attributeValue instanceof org.opensaml.saml.saml2.core.AttributeValue) {
org.opensaml.saml.saml2.core.AttributeValue attrValue =
(org.opensaml.saml.saml2.core.AttributeValue) attributeValue;
if (attrValue.getDOM() != null) {
String value = attrValue.getDOM().getTextContent();
if (value != null && !value.trim().isEmpty()) {
attributeValues.add(value.trim());
}
} else if (attrValue.getTextContent() != null) {
String value = attrValue.getTextContent();
if (!value.trim().isEmpty()) {
attributeValues.add(value.trim());
}
}
}
}
if (!attributeValues.isEmpty()) {
claims.add(attributeName, attributeValues);
}
}
}
}
return claims;
}
private static X509Certificate getCertificateFromString(String certString) throws CertificateException {
byte[] certBytes = java.util.Base64.getDecoder().decode(certString);
java.security.cert.CertificateFactory certFactory =
java.security.cert.CertificateFactory.getInstance("X.509");
return (X509Certificate) certFactory.generateCertificate(
new ByteArrayInputStream(certBytes));
}
public static JsonObject getUserInfo(Main main, TenantIdentifier tenantIdentifier, Storage storage, String accessToken, String clientId, boolean isLegacy)
throws TenantOrAppNotFoundException, StorageQueryException,
StorageTransactionLogicException, InvalidCodeException, FeatureNotEnabledException {
checkForSAMLFeature(tenantIdentifier.toAppIdentifier(), main);
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
SAMLClaimsInfo claimsInfo = samlStorage.getSAMLClaimsAndRemoveCode(tenantIdentifier, accessToken);
if (claimsInfo == null) {
throw new InvalidCodeException();
}
if (clientId != null) {
if (!clientId.equals(claimsInfo.clientId)) {
throw new InvalidCodeException();
}
}
String sub = null;
String email = null;
JsonObject claims = claimsInfo.claims;
if (claims.has("NameID")) {
sub = claims.getAsJsonArray("NameID").get(0).getAsString();
} else if (claims.has("http://schemas.microsoft.com/identity/claims/objectidentifier")) {
sub = claims.getAsJsonArray("http://schemas.microsoft.com/identity/claims/objectidentifier")
.get(0).getAsString();
} else if (claims.has("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")) {
sub = claims.getAsJsonArray("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")
.get(0).getAsString();
}
if (claims.has("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")) {
email = claims.getAsJsonArray("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")
.get(0).getAsString();
} else if (claims.has("NameID")) {
String nameIdValue = claims.getAsJsonArray("NameID").get(0).getAsString();
if (nameIdValue.contains("@")) {
email = nameIdValue;
}
}
JsonObject payload = new JsonObject();
payload.add("claims", claims);
payload.addProperty(isLegacy ? "id" : "sub", sub);
payload.addProperty("email", email);
payload.addProperty("aud", claimsInfo.clientId);
return payload;
}
public static String getLegacyACSURL(Main main, AppIdentifier appIdentifier) throws TenantOrAppNotFoundException {
CoreConfig config = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main);
return config.getSAMLLegacyACSURL();
}
public static String getMetadataXML(Main main, TenantIdentifier tenantIdentifier)
throws TenantOrAppNotFoundException, StorageQueryException, FeatureNotEnabledException {
checkForSAMLFeature(tenantIdentifier.toAppIdentifier(), main);
SAMLCertificate certificate = SAMLCertificate.getInstance(tenantIdentifier.toAppIdentifier(), main);
CoreConfig config = Config.getConfig(tenantIdentifier, main);
String spEntityId = config.getSAMLSPEntityID();
try {
X509Certificate cert = certificate.getCertificate();
String certString = java.util.Base64.getEncoder().encodeToString(cert.getEncoded());
String validUntil = java.time.format.DateTimeFormatter.ISO_INSTANT.format(cert.getNotAfter().toInstant());
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>");
sb.append("<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"")
.append(escapeXml(spEntityId)).append("\" validUntil=\"")
.append(escapeXml(validUntil)).append("\">");
sb.append("<md:SPSSODescriptor protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">");
sb.append("<md:KeyDescriptor use=\"signing\">");
sb.append("<ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">");
sb.append("<ds:X509Data>");
sb.append("<ds:X509Certificate>").append(certString).append("</ds:X509Certificate>");
sb.append("</ds:X509Data>");
sb.append("</ds:KeyInfo>");
sb.append("</md:KeyDescriptor>");
sb.append("<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>");
sb.append("</md:SPSSODescriptor>");
sb.append("</md:EntityDescriptor>");
return sb.toString();
} catch (Exception e) {
throw new IllegalStateException("Failed to generate SP metadata", e);
}
}
private static String escapeXml(String input) {
if (input == null) {
return "";
}
String result = input;
result = result.replace("&", "&amp;");
result = result.replace("\"", "&quot;");
result = result.replace("<", "&lt;");
result = result.replace(">", "&gt;");
return result;
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (c) 2025, 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.saml;
import java.util.HashMap;
import java.util.Map;
import org.opensaml.core.config.InitializationException;
import org.opensaml.core.config.InitializationService;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
public class SAMLBootstrap {
private static volatile boolean initialized = false;
private SAMLBootstrap() {}
public static void initialize() {
if (initialized) {
return;
}
synchronized (SAMLBootstrap.class) {
if (initialized) {
return;
}
try {
InitializationService.initialize();
initialized = true;
} catch (InitializationException e) {
throw new RuntimeException("Failed to initialize OpenSAML", e);
}
}
}
}

View File

@ -0,0 +1,315 @@
/*
* Copyright (c) 2025, 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.saml;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import io.supertokens.Main;
import io.supertokens.ResourceDistributor;
import io.supertokens.output.Logging;
import io.supertokens.pluginInterface.KeyValueInfo;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.sqlStorage.SQLStorage;
import io.supertokens.storageLayer.StorageLayer;
public class SAMLCertificate extends ResourceDistributor.SingletonResource {
private static final String RESOURCE_KEY = "io.supertokens.saml.SAMLCertificate";
private final Main main;
private final AppIdentifier appIdentifier;
private static final String SAML_KEY_PAIR_NAME = "saml_key_pair";
private static final String SAML_CERTIFICATE_NAME = "saml_certificate";
private KeyPair spKeyPair = null;
private X509Certificate spCertificate = null;
private SAMLCertificate(AppIdentifier appIdentifier, Main main) throws
TenantOrAppNotFoundException {
this.main = main;
this.appIdentifier = appIdentifier;
try {
if (!Main.isTesting) {
// Creation of new certificate is slow, not really necessary to create one for each test
this.getCertificate();
}
} catch (StorageQueryException | TenantOrAppNotFoundException e) {
Logging.error(main, appIdentifier.getAsPublicTenantIdentifier(), "Error while fetching SAML key and certificate",
false, e);
}
}
public synchronized X509Certificate getCertificate()
throws StorageQueryException, TenantOrAppNotFoundException {
if (this.spCertificate == null || this.spCertificate.getNotAfter().before(new Date())) {
maybeGenerateNewCertificateAndUpdateInDb();
}
return this.spCertificate;
}
private void maybeGenerateNewCertificateAndUpdateInDb() throws TenantOrAppNotFoundException {
SQLStorage storage = (SQLStorage) StorageLayer.getStorage(
this.appIdentifier.getAsPublicTenantIdentifier(), main);
try {
storage.startTransaction(con -> {
KeyValueInfo keyPairInfo = storage.getKeyValue_Transaction(this.appIdentifier.getAsPublicTenantIdentifier(), con, SAML_KEY_PAIR_NAME);
KeyValueInfo certInfo = storage.getKeyValue_Transaction(this.appIdentifier.getAsPublicTenantIdentifier(), con, SAML_CERTIFICATE_NAME);
if (keyPairInfo == null || certInfo == null) {
try {
generateNewCertificate();
} catch (Exception e) {
throw new RuntimeException(e);
}
try {
String keyPairStr = serializeKeyPair(spKeyPair);
String certStr = serializeCertificate(spCertificate);
keyPairInfo = new KeyValueInfo(keyPairStr);
certInfo = new KeyValueInfo(certStr);
} catch (IOException e) {
throw new RuntimeException("Failed to serialize key pair or certificate", e);
}
storage.setKeyValue_Transaction(this.appIdentifier.getAsPublicTenantIdentifier(), con, SAML_KEY_PAIR_NAME, keyPairInfo);
storage.setKeyValue_Transaction(this.appIdentifier.getAsPublicTenantIdentifier(), con, SAML_CERTIFICATE_NAME, certInfo);
}
String keyPairStr = keyPairInfo.value;
String certStr = certInfo.value;
try {
this.spKeyPair = deserializeKeyPair(keyPairStr);
this.spCertificate = deserializeCertificate(certStr);
} catch (Exception e) {
throw new RuntimeException("Failed to deserialize key pair or certificate", e);
}
// If the certificate has expired, generate and persist a new one
if (this.spCertificate.getNotAfter().before(new Date())) {
try {
generateNewCertificate();
String newKeyPairStr = serializeKeyPair(spKeyPair);
String newCertStr = serializeCertificate(spCertificate);
KeyValueInfo newKeyPairInfo = new KeyValueInfo(newKeyPairStr);
KeyValueInfo newCertInfo = new KeyValueInfo(newCertStr);
storage.setKeyValue_Transaction(this.appIdentifier.getAsPublicTenantIdentifier(), con, SAML_KEY_PAIR_NAME, newKeyPairInfo);
storage.setKeyValue_Transaction(this.appIdentifier.getAsPublicTenantIdentifier(), con, SAML_CERTIFICATE_NAME, newCertInfo);
} catch (Exception e) {
throw new RuntimeException("Failed to regenerate expired certificate", e);
}
}
return null;
});
} catch (StorageTransactionLogicException | StorageQueryException e) {
throw new RuntimeException("Storage error", e);
}
}
void generateNewCertificate()
throws NoSuchAlgorithmException, CertificateException, OperatorCreationException, CertIOException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(4096);
spKeyPair = keyGen.generateKeyPair();
spCertificate = generateSelfSignedCertificate();
}
private X509Certificate generateSelfSignedCertificate()
throws CertIOException, OperatorCreationException, CertificateException {
// Create a production-ready self-signed X.509 certificate using BouncyCastle
Date notBefore = new Date();
Date notAfter = new Date(notBefore.getTime() + 10 * 365L * 24 * 60 * 60 * 1000); // 10 year validity
// Create the certificate subject and issuer (same for self-signed)
X500Name subject = new X500Name("CN=SAML-SP, O=SuperTokens, C=US");
X500Name issuer = subject; // Self-signed
// Generate a random serial number (128 bits for good uniqueness)
SecureRandom random = new SecureRandom();
java.math.BigInteger serialNumber = new java.math.BigInteger(128, random);
// Create the certificate builder
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuer,
serialNumber,
notBefore,
notAfter,
subject,
spKeyPair.getPublic()
);
// Add extensions for proper SAML usage
// Key Usage: digitalSignature and keyEncipherment
KeyUsage keyUsage = new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment);
certBuilder.addExtension(Extension.keyUsage, true, keyUsage);
// Basic Constraints: not a CA
BasicConstraints basicConstraints = new BasicConstraints(false);
certBuilder.addExtension(Extension.basicConstraints, true, basicConstraints);
// Create the content signer
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA")
.build(spKeyPair.getPrivate());
// Build the certificate
X509CertificateHolder certHolder = certBuilder.build(contentSigner);
// Convert to standard X509Certificate
JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
return converter.getCertificate(certHolder);
}
/**
* Serializes a KeyPair to a Base64 encoded string format
*/
private String serializeKeyPair(KeyPair keyPair) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Write private key
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
baos.write(Base64.getEncoder().encode(privateKeyBytes));
baos.write('\n');
// Write public key
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
baos.write(Base64.getEncoder().encode(publicKeyBytes));
return baos.toString();
}
/**
* Deserializes a KeyPair from a Base64 encoded string format
*/
private KeyPair deserializeKeyPair(String keyPairStr) throws Exception {
String[] parts = keyPairStr.split("\n");
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid key pair string format");
}
// Decode private key
byte[] privateKeyBytes = Base64.getDecoder().decode(parts[0]);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
// Decode public key
byte[] publicKeyBytes = Base64.getDecoder().decode(parts[1]);
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
return new KeyPair(publicKey, privateKey);
}
/**
* Serializes an X509Certificate to a Base64 encoded string format
*/
private String serializeCertificate(X509Certificate certificate) throws IOException {
try {
byte[] certBytes = certificate.getEncoded();
return Base64.getEncoder().encodeToString(certBytes);
} catch (CertificateException e) {
throw new IOException("Failed to encode certificate", e);
}
}
/**
* Deserializes an X509Certificate from a Base64 encoded string format
*/
private X509Certificate deserializeCertificate(String certStr) throws Exception {
try {
byte[] certBytes = Base64.getDecoder().decode(certStr);
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
ByteArrayInputStream bais = new ByteArrayInputStream(certBytes);
return (X509Certificate) certFactory.generateCertificate(bais);
} catch (CertificateException e) {
throw new Exception("Failed to decode certificate", e);
}
}
public static SAMLCertificate getInstance(AppIdentifier appIdentifier, Main main)
throws TenantOrAppNotFoundException {
return (SAMLCertificate) main.getResourceDistributor()
.getResource(appIdentifier, RESOURCE_KEY);
}
public static void loadForAllTenants(Main main, List<AppIdentifier> apps,
List<TenantIdentifier> tenantsThatChanged) {
try {
main.getResourceDistributor().withResourceDistributorLock(() -> {
Map<ResourceDistributor.KeyClass, ResourceDistributor.SingletonResource> existingResources =
main.getResourceDistributor()
.getAllResourcesWithResourceKey(RESOURCE_KEY);
main.getResourceDistributor().clearAllResourcesWithResourceKey(RESOURCE_KEY);
for (AppIdentifier app : apps) {
ResourceDistributor.SingletonResource resource = existingResources.get(
new ResourceDistributor.KeyClass(app, RESOURCE_KEY));
if (resource != null && !tenantsThatChanged.contains(app.getAsPublicTenantIdentifier())) {
main.getResourceDistributor().setResource(app, RESOURCE_KEY,
resource);
} else {
try {
main.getResourceDistributor()
.setResource(app, RESOURCE_KEY,
new SAMLCertificate(app, main));
} catch (TenantOrAppNotFoundException e) {
Logging.error(main, app.getAsPublicTenantIdentifier(), e.getMessage(), false);
// continue loading other resources
}
}
}
return null;
});
} catch (ResourceDistributor.FuncException e) {
throw new IllegalStateException("should never happen", e);
}
}
}

View File

@ -0,0 +1,4 @@
package io.supertokens.saml.exceptions;
public class IDPInitiatedLoginDisallowedException extends Exception {
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) 2025, 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.saml.exceptions;
public class InvalidClientException extends Exception {
}

View File

@ -0,0 +1,5 @@
package io.supertokens.saml.exceptions;
public class InvalidCodeException extends Exception {
}

View File

@ -0,0 +1,5 @@
package io.supertokens.saml.exceptions;
public class InvalidRelayStateException extends Exception {
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) 2025, 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.saml.exceptions;
public class MalformedSAMLMetadataXMLException extends Exception {
}

View File

@ -0,0 +1,5 @@
package io.supertokens.saml.exceptions;
public class SAMLResponseVerificationFailedException extends Exception {
}

View File

@ -19,6 +19,7 @@ package io.supertokens.session;
import com.google.gson.JsonObject;
import io.supertokens.Main;
import io.supertokens.ProcessState;
import io.supertokens.ResourceDistributor;
import io.supertokens.config.Config;
import io.supertokens.config.CoreConfig;
import io.supertokens.exceptions.AccessTokenPayloadError;
@ -100,7 +101,7 @@ public class Session {
Storage storage = StorageLayer.getStorage(main);
try {
return createNewSession(
new TenantIdentifier(null, null, null), storage, main,
ResourceDistributor.getAppForTesting(), storage, main,
recipeUserId, userDataInJWT, userDataInDatabase, false, AccessToken.getLatestVersion(), false);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -120,7 +121,7 @@ public class Session {
Storage storage = StorageLayer.getStorage(main);
try {
return createNewSession(
new TenantIdentifier(null, null, null), storage, main,
ResourceDistributor.getAppForTesting(), storage, main,
recipeUserId, userDataInJWT, userDataInDatabase, enableAntiCsrf, version, useStaticKey);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -200,7 +201,7 @@ public class Session {
InvalidKeyException, JWT.JWTException,
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError, TryRefreshTokenException {
try {
return regenerateToken(new AppIdentifier(null, null), main, token, userDataInJWT);
return regenerateToken(ResourceDistributor.getAppForTesting().toAppIdentifier(), main, token, userDataInJWT);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -326,7 +327,7 @@ public class Session {
StorageTransactionLogicException, TryRefreshTokenException, UnauthorisedException,
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError {
try {
return getSession(new AppIdentifier(null, null), main, token, antiCsrfToken, enableAntiCsrf,
return getSession(ResourceDistributor.getAppForTesting().toAppIdentifier(), main, token, antiCsrfToken, enableAntiCsrf,
doAntiCsrfCheck, checkDatabase);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -532,7 +533,7 @@ public class Session {
UnauthorisedException, StorageQueryException, TokenTheftDetectedException,
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError {
try {
return refreshSession(new AppIdentifier(null, null), main, refreshToken, antiCsrfToken,
return refreshSession(ResourceDistributor.getAppForTesting().toAppIdentifier(), main, refreshToken, antiCsrfToken,
enableAntiCsrf, accessTokenVersion, null);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -768,7 +769,7 @@ public class Session {
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return revokeSessionUsingSessionHandles(main,
new AppIdentifier(null, null), storage,
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
sessionHandles);
}
@ -860,7 +861,7 @@ public class Session {
public static String[] revokeAllSessionsForUser(Main main, String userId) throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return revokeAllSessionsForUser(main,
new AppIdentifier(null, null), storage, userId, true);
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId, true);
}
public static String[] revokeAllSessionsForUser(Main main, AppIdentifier appIdentifier,
@ -886,7 +887,7 @@ public class Session {
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getAllNonExpiredSessionHandlesForUser(main,
new AppIdentifier(null, null), storage, userId, true);
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId, true);
}
public static String[] getAllNonExpiredSessionHandlesForUser(
@ -957,7 +958,7 @@ public class Session {
throws StorageQueryException, UnauthorisedException {
Storage storage = StorageLayer.getStorage(main);
return getSessionData(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
sessionHandle);
}
@ -978,7 +979,7 @@ public class Session {
throws StorageQueryException, UnauthorisedException {
Storage storage = StorageLayer.getStorage(main);
return getJWTData(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
sessionHandle);
}
@ -998,7 +999,7 @@ public class Session {
throws StorageQueryException, UnauthorisedException {
Storage storage = StorageLayer.getStorage(main);
return getSession(
new TenantIdentifier(null, null, null), storage,
ResourceDistributor.getAppForTesting(), storage,
sessionHandle);
}
@ -1028,7 +1029,7 @@ public class Session {
AccessToken.VERSION version)
throws StorageQueryException, UnauthorisedException, AccessTokenPayloadError {
Storage storage = StorageLayer.getStorage(main);
updateSession(new TenantIdentifier(null, null, null), storage,
updateSession(ResourceDistributor.getAppForTesting(), storage,
sessionHandle, sessionData, jwtData, version);
}

Some files were not shown because too many files have changed in this diff Show More