Compare commits

...

83 Commits
10.1 ... 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
Viraj Kanwade 4db84200fa docs(README.md): updated github link for Viraj Kanwade 2025-01-13 21:55:08 -08:00
470 changed files with 15464 additions and 4452 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

1
.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

View File

@ -7,6 +7,117 @@ 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

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,18 +21,25 @@ compileTestJava { options.encoding = "UTF-8" }
// }
//}
version = "10.1.4"
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.18.2'
@ -41,11 +50,8 @@ dependencies {
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.2'
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14'
// 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'
@ -80,6 +86,28 @@ dependencies {
// 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")
// this is so that we can find plugin-interface jar while testing
@ -107,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

@ -182,3 +182,22 @@ core_config_version: 0
# (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

@ -22,6 +22,7 @@
"5.0",
"5.1",
"5.2",
"5.3"
"5.3",
"5.4"
]
}

View File

@ -182,3 +182,22 @@ disable_telemetry: true
# (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,135 +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.18.2/jackson-dataformat-yaml-2.18.2.jar",
"name": "Jackson Dataformat 2.18.2",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.18.2/jackson-dataformat-yaml-2.18.2-sources.jar"
},
{
"jar": "https://repo1.maven.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://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.18.2/jackson-dataformat-cbor-2.18.2-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.3/snakeyaml-2.3.jar",
"name": "SnakeYAML 2.3",
"src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.3/snakeyaml-2.3-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.18.2/jackson-core-2.18.2.jar",
"name": "Jackson core 2.18.2",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.18.2/jackson-core-2.18.2-sources.jar"
},
{
"jar": "https://repo1.maven.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://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.18.2/jackson-databind-2.18.2-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.18.2/jackson-annotations-2.18.2.jar",
"name": "Jackson annotation 2.18.2",
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.18.2/jackson-annotations-2.18.2-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.16/slf4j-api-2.0.16.jar",
"name": "SLF4j API 2.0.16",
"src": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.16/slf4j-api-2.0.16-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"
},
{
"jar": "https://repo1.maven.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://repo1.maven.org/maven2/com/webauthn4j/webauthn4j-core/0.28.6.RELEASE/webauthn4j-core-0.28.6.RELEASE-sources.jar"
},
{
"jar": "https://repo1.maven.org/maven2/com/webauthn4j/webauthn4j-util/0.28.6.RELEASE/webauthn4j-util-0.28.6.RELEASE.jar",
"name": "webauthn4j-util 0.28.6.RELEASE",
"src": "https://repo1.maven.org/maven2/com/webauthn4j/webauthn4j-util/0.28.6.RELEASE/webauthn4j-util-0.28.6.RELEASE-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.1"
"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,7 +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;
@ -42,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;
@ -120,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;
}
@ -154,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);
}
@ -164,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);
}
@ -177,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);
@ -273,6 +284,13 @@ public class Main {
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();
@ -362,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()) {
@ -410,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
@ -441,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,6 +17,7 @@
package io.supertokens.authRecipe;
import io.supertokens.Main;
import io.supertokens.ResourceDistributor;
import io.supertokens.authRecipe.exception.*;
import io.supertokens.bulkimport.BulkImportUserUtils;
import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
@ -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)
@ -203,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);
}
@ -508,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);
@ -648,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,
@ -920,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);
}
@ -1181,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);
}
@ -1231,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);
@ -1389,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);
@ -1400,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

@ -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;
@ -209,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);
@ -225,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){
@ -236,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);
@ -288,9 +311,9 @@ public class BulkImport {
}
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()) {
@ -664,7 +687,7 @@ public class BulkImport {
rolesToUserByTenant.put(tenantIdentifier, new HashMap<>());
}
String userIdToUse = user.externalUserId != null ?
user.externalUserId : user.primaryUserId;
user.externalUserId : user.id;
if(!rolesToUserByTenant.get(tenantIdentifier).containsKey(userIdToUse)){
rolesToUserByTenant.get(tenantIdentifier).put(userIdToUse, new ArrayList<>());
}
@ -683,12 +706,31 @@ public class BulkImport {
Map<String, String> emailToUserId = collectVerifiedEmailAddressesByUserIds(users);
try {
verifyCollectedEmailAddressesForUsers(appIdentifier, storage, emailToUserId);
} catch (StorageQueryException e) {
} 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)
private static void verifyCollectedEmailAddressesForUsers(AppIdentifier appIdentifier, Storage storage,
Map<String, String> emailToUserId)
throws StorageQueryException, StorageTransactionLogicException {
if(!emailToUserId.isEmpty()) {
EmailVerificationSQLStorage emailVerificationSQLStorage = StorageUtils
@ -706,10 +748,11 @@ public class BulkImport {
@NotNull
private static Map<String, String> collectVerifiedEmailAddressesByUserIds(List<BulkImportUser> users) {
Map<String, String> emailToUserId = new HashMap<>();
Map<String, String> emailToUserId = new LinkedHashMap<>();
for (BulkImportUser user : users) {
for (LoginMethod lm : user.loginMethods) {
if(lm.isVerified) {
//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);
}

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,29 +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;
@ -429,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;
@ -608,12 +696,76 @@ public class CoreConfig {
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;
@ -836,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("")) {
@ -973,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

@ -109,6 +109,9 @@ public class ProcessBulkImportUsers extends CronTask {
}
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<>();
@ -124,14 +127,26 @@ public class ProcessBulkImportUsers extends CronTask {
Thread.sleep(1000);
}
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Task " + task + " finished");
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
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<>();
@ -106,8 +108,8 @@ 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) {
@ -126,11 +128,13 @@ public class ProcessBulkUsersImportWorker implements Runnable {
while (true){
try {
baseTenantStorage.deleteBulkImportUsers(appIdentifier, toDelete);
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());
Logging.debug(main, app.getAsPublicTenantIdentifier(),
"Exception while deleting bulk import users: " + e.getMessage());
}
}
} catch (StorageTransactionLogicException | StorageQueryException e) {
@ -148,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
}
@ -173,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 {
@ -294,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,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;
@ -116,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, WebAuthNSQLStorage {
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";
@ -202,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
}
@ -227,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
@ -619,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 {
@ -759,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);
@ -1016,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 {
@ -2748,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);
}
@ -3889,4 +3903,72 @@ public class Start
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

@ -194,4 +194,10 @@ public class SQLiteConfig {
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

@ -516,6 +516,33 @@ public class GeneralQueries {
//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,
@ -1275,7 +1302,8 @@ public class GeneralQueries {
userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail_Transaction(start, sqlCon, appIdentifier, email));
String webauthnUserId = WebAuthNQueries.getPrimaryUserIdUsingEmail_Transaction(start, sqlCon, appIdentifier, email);
String webauthnUserId = WebAuthNQueries.getPrimaryUserIdForAppUsingEmail_Transaction(start, sqlCon,
appIdentifier, email);
if(webauthnUserId != null) {
userIds.add(webauthnUserId);
}
@ -1312,7 +1340,7 @@ public class GeneralQueries {
userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier, email));
String webauthnUserId = WebAuthNQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier.toAppIdentifier(), email);
String webauthnUserId = WebAuthNQueries.getPrimaryUserIdForTenantUsingEmail(start, tenantIdentifier, email);
if(webauthnUserId != null) {
userIds.add(webauthnUserId);
}

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

@ -376,13 +376,15 @@ public class WebAuthNQueries {
return userInfo;
}
public static String getPrimaryUserIdUsingEmail(Start start, AppIdentifier appIdentifier, String email)
public static String getPrimaryUserIdForTenantUsingEmail(Start start, TenantIdentifier tenantIdentifier,
String email)
throws StorageQueryException {
try {
return start.startTransaction(con -> {
try {
Connection sqlConnection = (Connection) con.getConnection();
return getPrimaryUserIdUsingEmail_Transaction(start, sqlConnection, appIdentifier, email);
return getPrimaryUserIdForTenantUsingEmail_Transaction(start, sqlConnection, tenantIdentifier,
email);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
@ -392,7 +394,30 @@ public class WebAuthNQueries {
}
}
public static String getPrimaryUserIdUsingEmail_Transaction(Start start, Connection sqlConnection, AppIdentifier appIdentifier, String email)
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" +
@ -405,7 +430,7 @@ public class WebAuthNQueries {
pst.setString(2, email);
}, result -> {
if (result.next()) {
return result.getString("user_id");
return result.getString("user_id");
}
return null;
});

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);
}

View File

@ -21,6 +21,7 @@ import com.google.gson.JsonObject;
import io.supertokens.Main;
import io.supertokens.ProcessState;
import io.supertokens.ProcessState.PROCESS_STATE;
import io.supertokens.ResourceDistributor;
import io.supertokens.config.Config;
import io.supertokens.exceptions.AccessTokenPayloadError;
import io.supertokens.exceptions.TryRefreshTokenException;
@ -175,7 +176,7 @@ public class AccessToken {
throws StorageQueryException, StorageTransactionLogicException, TryRefreshTokenException,
UnsupportedJWTSigningAlgorithmException {
try {
return getInfoFromAccessToken(new AppIdentifier(null, null), main, token, doAntiCsrfCheck);
return getInfoFromAccessToken(ResourceDistributor.getAppForTesting().toAppIdentifier(), main, token, doAntiCsrfCheck);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -186,7 +187,7 @@ public class AccessToken {
public static AccessTokenInfo getInfoFromAccessTokenWithoutVerifying(@Nonnull String token)
throws JWTException, TryRefreshTokenException {
return getInfoFromAccessTokenWithoutVerifying(
new AppIdentifier(null, null), token);
ResourceDistributor.getAppForTesting().toAppIdentifier(), token);
}
public static AccessTokenInfo getInfoFromAccessTokenWithoutVerifying(AppIdentifier appIdentifier,
@ -209,7 +210,7 @@ public class AccessToken {
NoSuchAlgorithmException, InvalidKeySpecException, SignatureException,
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError {
try {
return createNewAccessToken(new TenantIdentifier(null, null, null), main, sessionHandle, userId, userId,
return createNewAccessToken(ResourceDistributor.getAppForTesting(), main, sessionHandle, userId, userId,
refreshTokenHash1,
parentRefreshTokenHash1,
userData, antiCsrfToken, expiryTime, version, useStaticKey);
@ -275,7 +276,7 @@ public class AccessToken {
NoSuchAlgorithmException, InvalidKeySpecException, SignatureException,
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError {
try {
return createNewAccessTokenV1(new TenantIdentifier(null, null, null), main, sessionHandle, userId,
return createNewAccessTokenV1(ResourceDistributor.getAppForTesting(), main, sessionHandle, userId,
refreshTokenHash1,
parentRefreshTokenHash1, userData, antiCsrfToken);
} catch (TenantOrAppNotFoundException e) {

View File

@ -18,6 +18,7 @@ package io.supertokens.session.refreshToken;
import com.google.gson.Gson;
import io.supertokens.Main;
import io.supertokens.ResourceDistributor;
import io.supertokens.config.Config;
import io.supertokens.exceptions.UnauthorisedException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
@ -46,7 +47,7 @@ public class RefreshToken {
public static RefreshTokenInfo getInfoFromRefreshToken(@Nonnull Main main, @Nonnull String token)
throws UnauthorisedException, StorageQueryException, StorageTransactionLogicException {
try {
return getInfoFromRefreshToken(new AppIdentifier(null, null), main, token);
return getInfoFromRefreshToken(ResourceDistributor.getAppForTesting().toAppIdentifier(), main, token);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -92,7 +93,7 @@ public class RefreshToken {
IllegalBlockSizeException, BadPaddingException, StorageTransactionLogicException,
InvalidAlgorithmParameterException, InvalidKeySpecException {
try {
return createNewRefreshToken(new TenantIdentifier(null, null, null), main, sessionHandle, userId,
return createNewRefreshToken(ResourceDistributor.getAppForTesting(), main, sessionHandle, userId,
parentRefreshTokenHash1,
antiCsrfToken);
} catch (TenantOrAppNotFoundException e) {

View File

@ -85,7 +85,7 @@ public class AccessTokenSigningKey extends ResourceDistributor.SingletonResource
@TestOnly
public static AccessTokenSigningKey getInstance(Main main) {
try {
return getInstance(new AppIdentifier(null, null), main);
return getInstance(ResourceDistributor.getAppForTesting().toAppIdentifier(), main);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}

View File

@ -63,7 +63,7 @@ public class SigningKeys extends ResourceDistributor.SingletonResource {
@TestOnly
public static SigningKeys getInstance(Main main) {
try {
return getInstance(new AppIdentifier(null, null), main);
return getInstance(ResourceDistributor.getAppForTesting().toAppIdentifier(), main);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}

View File

@ -36,7 +36,9 @@ import io.supertokens.pluginInterface.multitenancy.MultitenancyStorage;
import io.supertokens.pluginInterface.multitenancy.TenantConfig;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.opentelemetry.WithinOtelSpan;
import io.supertokens.pluginInterface.useridmapping.UserIdMapping;
import io.supertokens.telemetry.TelemetryProvider;
import io.supertokens.useridmapping.UserIdType;
import jakarta.servlet.ServletException;
import org.jetbrains.annotations.TestOnly;
@ -48,11 +50,13 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
@WithinOtelSpan
public class StorageLayer extends ResourceDistributor.SingletonResource {
public static final String RESOURCE_KEY = "io.supertokens.storageLayer.StorageLayer";
private final Storage storage;
private static URLClassLoader ucl = null;
private static Storage storageInstanceForEnv = null;
public Storage getUnderlyingStorage() {
return storage;
@ -66,6 +70,37 @@ public class StorageLayer extends ResourceDistributor.SingletonResource {
return getNewInstance(main, config, tenantIdentifier, doNotLog, true);
}
public static void updateConfigJsonFromEnv(Main main, JsonObject configJson) {
if (storageInstanceForEnv == null) {
Storage result;
if (StorageLayer.ucl == null) {
result = new Start(main);
} else {
Storage storageLayer = null;
ServiceLoader<Storage> sl = ServiceLoader.load(Storage.class, ucl);
for (Storage plugin : sl) {
if (storageLayer == null) {
storageLayer = plugin;
} else {
throw new QuitProgramException(
"Multiple database plugins found. Please make sure that just one plugin is in the "
+ "/plugin" + " "
+ "folder of the installation. Alternatively, please redownload and install "
+ "SuperTokens" + ".");
}
}
if (storageLayer != null) {
result = storageLayer;
} else {
result = new Start(main);
}
}
storageInstanceForEnv = result;
}
storageInstanceForEnv.updateConfigJsonFromEnv(configJson);
}
private static Storage getNewInstance(Main main, JsonObject config, TenantIdentifier tenantIdentifier, boolean doNotLog, boolean isBulkImportProxy) throws InvalidConfigException {
Storage result;
if (StorageLayer.ucl == null) {
@ -85,7 +120,7 @@ public class StorageLayer extends ResourceDistributor.SingletonResource {
}
}
if (storageLayer != null && !main.isForceInMemoryDB()
&& (storageLayer.canBeUsed(config) || CLIOptions.get(main).isForceNoInMemoryDB())) {
&& (storageLayer. canBeUsed(config) || CLIOptions.get(main).isForceNoInMemoryDB())) {
if (isBulkImportProxy) {
result = storageLayer.createBulkImportProxyStorageInstance();
} else {
@ -115,9 +150,7 @@ public class StorageLayer extends ResourceDistributor.SingletonResource {
this.storage = storage;
}
private StorageLayer(Main main, String pluginFolderPath, JsonObject configJson, TenantIdentifier tenantIdentifier)
throws MalformedURLException, InvalidConfigException {
Logging.info(main, tenantIdentifier, "Loading storage layer.", true);
public static void loadStorageUCL(String pluginFolderPath) throws MalformedURLException {
File loc = new File(pluginFolderPath);
File[] flist = loc.listFiles(file -> file.getPath().toLowerCase().endsWith(".jar"));
@ -136,6 +169,12 @@ public class StorageLayer extends ResourceDistributor.SingletonResource {
}
}
}
private StorageLayer(Main main, JsonObject configJson, TenantIdentifier tenantIdentifier)
throws InvalidConfigException {
Logging.info(main, tenantIdentifier, "Loading storage layer.", true);
this.storage = getNewStorageInstance(main, configJson, tenantIdentifier, false);
if (this.storage instanceof Start) {
@ -203,10 +242,10 @@ public class StorageLayer extends ResourceDistributor.SingletonResource {
return (StorageLayer) main.getResourceDistributor().getResource(tenantIdentifier, RESOURCE_KEY);
}
public static void initPrimary(Main main, String pluginFolderPath, JsonObject configJson)
public static void initPrimary(Main main, JsonObject configJson)
throws MalformedURLException, InvalidConfigException {
main.getResourceDistributor().setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY,
new StorageLayer(main, pluginFolderPath, configJson, TenantIdentifier.BASE_TENANT));
new StorageLayer(main, configJson, TenantIdentifier.BASE_TENANT));
}
public static void loadAllTenantStorage(Main main, TenantConfig[] tenants)
@ -321,7 +360,8 @@ public class StorageLayer extends ResourceDistributor.SingletonResource {
new ArrayList<>(storageToTenantIdentifiersMap.get(((StorageLayer) resource).storage)));
((StorageLayer) resource).storage.initFileLogging(
Config.getBaseConfig(main).getInfoLogPath(main),
Config.getBaseConfig(main).getErrorLogPath(main));
Config.getBaseConfig(main).getErrorLogPath(main),
TelemetryProvider.getInstance(main));
} catch (DbInitException e) {
Logging.error(main, TenantIdentifier.BASE_TENANT, e.getMessage(), false, e);
@ -348,6 +388,7 @@ public class StorageLayer extends ResourceDistributor.SingletonResource {
}
}
@WithinOtelSpan
public static Storage getStorage(TenantIdentifier tenantIdentifier, Main main)
throws TenantOrAppNotFoundException {
return getInstance(tenantIdentifier, main).storage;

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