Compare commits

..

1 Commits
master ... 10.0

Author SHA1 Message Date
Tamas Soltesz bde7d07175
backport: logs to otel (#1170)
* backport: logs to otel

fix: add implementationDependencies.json dependencies

chore: build version and changelog

fix: add missing config and devConfig entries

fix: remove accidentally merged lines

* fix: for backport 10.0

* fix: oauth service

* fix: oauth provider

---------

Co-authored-by: Sattvik Chakravarthy <sattvik@gmail.com>
Co-authored-by: Sattvik Chakravarthy <sattvik@supertokens.com>
2025-08-18 14:08:30 +05:30
509 changed files with 4710 additions and 25516 deletions

View File

@ -1,64 +0,0 @@
FROM ubuntu:22.04
RUN apt-get update -y
#&& apt-get upgrade -y
RUN apt-get install build-essential -y --fix-missing
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 [ -d /var/run/mysqld ] || mkdir -p /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
RUN mkdir /usr/java
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://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 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-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

@ -1,57 +0,0 @@
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

@ -1,57 +0,0 @@
FROM ubuntu:18.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 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 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 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

@ -1,63 +0,0 @@
FROM ubuntu:22.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 [ -d /var/run/mysqld ] || mkdir -p /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
RUN mkdir /usr/java
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://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 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 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-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

@ -1,94 +0,0 @@
version: 2.1
orbs:
slack: circleci/slack@3.4.2
jobs:
test:
docker:
- image: tamassupertokens/supertokens_core_testing
- image: rishabhpoddar/oauth-server-cicd
- image: mongo
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: root
resource_class: large
parallelism: 4
parameters:
plugin:
type: string
steps:
- checkout
- run: mkdir ~/junit
- run: echo $'\n[mysqld]\ncharacter_set_server=utf8mb4\nmax_connections=10000' >> /etc/mysql/mysql.cnf
- run: echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/13/main/pg_hba.conf
- run: echo "listen_addresses='*'" >> /etc/postgresql/13/main/postgresql.conf
- run: sed -i 's/^#*\s*max_connections\s*=.*/max_connections = 10000/' /etc/postgresql/13/main/postgresql.conf
- run: (cd .circleci/ && ./doTests.sh << parameters.plugin >>)
- store_test_results:
path: ~/junit
- slack/status
mark-passed:
docker:
- image: tamassupertokens/supertokens_core_testing
steps:
- checkout
- run: (cd .circleci && ./markPassed.sh)
- slack/status
workflows:
version: 2
tagged-build:
jobs:
- test:
plugin: sqlite
name: test-sqlite
context:
- slack-notification
filters:
tags:
only: /dev-v[0-9]+(\.[0-9]+)*/
branches:
only: /test-cicd\/.*/
- test:
plugin: mongodb
name: test-mongodb
context:
- slack-notification
filters:
tags:
only: /dev-v[0-9]+(\.[0-9]+)*/
branches:
only: /test-cicd\/.*/
- test:
plugin: postgresql
name: test-postgresql
context:
- slack-notification
filters:
tags:
only: /dev-v[0-9]+(\.[0-9]+)*/
branches:
only: /test-cicd\/.*/
- test:
plugin: mysql
name: test-mysql
context:
- slack-notification
filters:
tags:
only: /dev-v[0-9]+(\.[0-9]+)*/
branches:
only: /test-cicd\/.*/
- mark-passed:
context:
- slack-notification
filters:
tags:
only: /dev-v[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/
requires:
- test-sqlite
- test-mongodb
- test-postgresql
- test-mysql

View File

@ -1,260 +0,0 @@
function cleanup {
if test -f "pluginInterfaceExactVersionsOutput"; then
rm pluginInterfaceExactVersionsOutput
fi
}
trap cleanup EXIT
cleanup
pluginToTest=$1
pinnedDBJson=$(curl -s -X GET \
'https://api.supertokens.io/0/plugin/pinned?planType=FREE' \
-H 'api-version: 0')
pinnedDBLength=$(echo "$pinnedDBJson" | jq ".plugins | length")
pinnedDBArray=$(echo "$pinnedDBJson" | jq ".plugins")
echo "got pinned dbs..."
pluginInterfaceJson=$(cat ../pluginInterfaceSupported.json)
pluginInterfaceLength=$(echo "$pluginInterfaceJson" | jq ".versions | length")
pluginInterfaceArray=$(echo "$pluginInterfaceJson" | jq ".versions")
echo "got plugin interface relations"
coreDriverJson=$(cat ../coreDriverInterfaceSupported.json)
coreDriverArray=$(echo "$coreDriverJson" | jq ".versions")
echo "got core driver relations"
./getPluginInterfaceExactVersions.sh "$pluginInterfaceLength" "$pluginInterfaceArray"
if [[ $? -ne 0 ]]
then
echo "all plugin interfaces found... failed. exiting!"
exit 1
else
echo "all plugin interfaces found..."
fi
# get core version
coreVersion=$(cat ../build.gradle | grep -e "version =" -e "version=")
while IFS='"' read -ra ADDR; do
counter=0
for i in "${ADDR[@]}"; do
if [ $counter == 1 ]
then
coreVersion=$i
fi
counter=$(($counter+1))
done
done <<< "$coreVersion"
responseStatus=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
https://api.supertokens.io/0/core \
-H 'Content-Type: application/json' \
-H 'api-version: 0' \
-d "{
\"password\": \"$SUPERTOKENS_API_KEY\",
\"planType\":\"FREE\",
\"version\":\"$coreVersion\",
\"pluginInterfaces\": $pluginInterfaceArray,
\"coreDriverInterfaces\": $coreDriverArray
}")
if [ "$responseStatus" -ne "200" ]
then
echo "failed core PUT API status code: $responseStatus. Exiting!"
exit 1
fi
mkdir -p ~/junit
someTestsRan=false
while read -u 10 line
do
if [[ $line = "" ]]; then
continue
fi
i=0
currTag=$(echo "$line" | jq .tag)
currTag=$(echo "$currTag" | tr -d '"')
currVersion=$(echo "$line" | jq .version)
currVersion=$(echo "$currVersion" | tr -d '"')
piX=$(cut -d'.' -f1 <<<"$currVersion")
piY=$(cut -d'.' -f2 <<<"$currVersion")
piVersion="$piX.$piY"
while [ $i -lt "$pinnedDBLength" ]; do
someTestsRan=true
currPinnedDb=$(echo "$pinnedDBArray" | jq ".[$i]")
currPinnedDb=$(echo "$currPinnedDb" | tr -d '"')
i=$((i+1))
if [[ $currPinnedDb == $pluginToTest ]]
then
echo ""
echo ""
echo ""
echo ""
echo ""
echo "===== testing $currPinnedDb with plugin-interface $currVersion ====="
echo ""
echo ""
echo ""
echo ""
echo ""
if [[ $currPinnedDb == "sqlite" ]]
then
# shellcheck disable=SC2034
continue=1
else
response=$(curl -s -X GET \
"https://api.supertokens.io/0/plugin-interface/dependency/plugin/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$piVersion&pluginName=$currPinnedDb" \
-H 'api-version: 0')
if [[ $(echo "$response" | jq .plugin) == "null" ]]
then
echo "fetching latest X.Y version for $currPinnedDb given plugin-interface X.Y version: $piVersion gave response: $response"
exit 1
fi
pinnedDbVersionX2=$(echo $response | jq .plugin | tr -d '"')
response=$(curl -s -X GET \
"https://api.supertokens.io/0/plugin/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$pinnedDbVersionX2&name=$currPinnedDb" \
-H 'api-version: 0')
if [[ $(echo "$response" | jq .tag) == "null" ]]
then
echo "fetching latest X.Y.Z version for $currPinnedDb, X.Y version: $pinnedDbVersionX2 gave response: $response"
exit 1
fi
pinnedDbVersionTag=$(echo "$response" | jq .tag | tr -d '"')
pinnedDbVersion=$(echo "$response" | jq .version | tr -d '"')
./startDb.sh "$currPinnedDb"
fi
cd ../../
git clone git@github.com:supertokens/supertokens-root.git
cd supertokens-root
rm gradle.properties
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")
if [[ $currPinnedDb == "sqlite" ]]
then
echo -e "core,$coreX.$coreY\nplugin-interface,$piVersion" > modules.txt
else
echo -e "core,$coreX.$coreY\nplugin-interface,$piVersion\n$currPinnedDb-plugin,$pinnedDbVersionX2" > modules.txt
fi
./loadModules
cd supertokens-core
git checkout dev-v$coreVersion
cd ../supertokens-plugin-interface
git checkout $currTag
if [[ $currPinnedDb == "sqlite" ]]
then
# shellcheck disable=SC2034
continue=1
else
cd ../supertokens-$currPinnedDb-plugin
git checkout $pinnedDbVersionTag
fi
cd ../
echo $SUPERTOKENS_API_KEY > apiPassword
./startTestingEnv --cicd
TEST_EXIT_CODE=$?
if [ -d ~/junit ]
then
echo "Copying output from core"
cp ~/supertokens-root/supertokens-core/build/test-results/test/*.xml ~/junit/
if [[ $pluginToTest != "sqlite" ]]
then
echo "Copying output from plugin"
cp ~/supertokens-root/supertokens-$pluginToTest-plugin/build/test-results/test/*.xml ~/junit/
fi
fi
if [[ $TEST_EXIT_CODE -ne 0 ]]
then
echo ""
echo ""
echo ""
echo ""
echo ""
echo "===== testing $currPinnedDb with plugin-interface $currVersion FAILED ====="
echo ""
echo ""
echo ""
echo ""
echo ""
cat logs/*
cd ../project/
echo "test failed... exiting!"
exit 1
fi
echo ""
echo ""
echo ""
echo ""
echo ""
echo "===== testing $currPinnedDb with plugin-interface $currVersion SUCCEEDED ====="
echo ""
echo ""
echo ""
echo ""
echo ""
cd ..
rm -rf supertokens-root
if [[ $currPinnedDb == "sqlite" ]]
then
# shellcheck disable=SC2034
continue=1
else
curl -o supertokens.zip -s -X GET \
"https://api.supertokens.io/0/app/download?pluginName=$currPinnedDb&os=linux&mode=DEV&binary=FREE&targetCore=$coreVersion&targetPlugin=$pinnedDbVersion" \
-H 'api-version: 0'
unzip supertokens.zip -d .
rm supertokens.zip
cd supertokens
../project/.circleci/testCli.sh
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
cd ../
fi
rm -rf supertokens
cd project/.circleci
if [[ $currPinnedDb == "sqlite" ]]
then
# shellcheck disable=SC2034
continue=1
else
./stopDb.sh $currPinnedDb
fi
fi
done
done 10<pluginInterfaceExactVersionsOutput
if [[ $someTestsRan = "true" ]]
then
echo "tests ran successfully"
else
echo "no test ran"
exit 1
fi

View File

@ -1,19 +0,0 @@
# args: <length of array> <array like ["0.0", "0.1"]>
touch pluginInterfaceExactVersionsOutput
i=0
while [ $i -lt $1 ]; do
currVersion=`echo $2 | jq ".[$i]"`
currVersion=`echo $currVersion | tr -d '"'`
i=$((i+1))
# now we have the current version like 0.0.
# We now have to find something that matches dev-v0.0.* or v0.0.*
response=`curl -s -X GET \
"https://api.supertokens.io/0/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$currVersion" \
-H 'api-version: 0'`
if [[ `echo $response | jq .tag` == "null" ]]
then
echo $response
exit 1
fi
echo $response >> pluginInterfaceExactVersionsOutput
done

View File

@ -1,29 +0,0 @@
coreVersion=$(cat ../build.gradle | grep -e "version =" -e "version=")
while IFS='"' read -ra ADDR; do
counter=0
for i in "${ADDR[@]}"; do
if [ $counter == 1 ]
then
coreVersion=$i
fi
counter=$(($counter+1))
done
done <<< "$coreVersion"
echo "calling /core PATCH to make testing passed"
responseStatus=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH \
https://api.supertokens.io/0/core \
-H 'Content-Type: application/json' \
-H 'api-version: 0' \
-d "{
\"password\": \"$SUPERTOKENS_API_KEY\",
\"planType\":\"FREE\",
\"version\":\"$coreVersion\",
\"testPassed\": true
}")
if [ "$responseStatus" -ne "200" ]
then
echo "patch api failed"
exit 1
fi

View File

@ -1 +0,0 @@
chown -R mysql:mysql /var/lib/mysql /var/run/mysqld && service mysql start

View File

@ -1,113 +0,0 @@
case $1 in
mysql)
(cd / && ./runMySQL.sh)
mysql -u root --password=root -e "CREATE DATABASE supertokens;"
mysql -u root --password=root -e "CREATE DATABASE st0;"
mysql -u root --password=root -e "CREATE DATABASE st1;"
mysql -u root --password=root -e "CREATE DATABASE st2;"
mysql -u root --password=root -e "CREATE DATABASE st3;"
mysql -u root --password=root -e "CREATE DATABASE st4;"
mysql -u root --password=root -e "CREATE DATABASE st5;"
mysql -u root --password=root -e "CREATE DATABASE st6;"
mysql -u root --password=root -e "CREATE DATABASE st7;"
mysql -u root --password=root -e "CREATE DATABASE st8;"
mysql -u root --password=root -e "CREATE DATABASE st9;"
mysql -u root --password=root -e "CREATE DATABASE st10;"
mysql -u root --password=root -e "CREATE DATABASE st11;"
mysql -u root --password=root -e "CREATE DATABASE st12;"
mysql -u root --password=root -e "CREATE DATABASE st13;"
mysql -u root --password=root -e "CREATE DATABASE st14;"
mysql -u root --password=root -e "CREATE DATABASE st15;"
mysql -u root --password=root -e "CREATE DATABASE st16;"
mysql -u root --password=root -e "CREATE DATABASE st17;"
mysql -u root --password=root -e "CREATE DATABASE st18;"
mysql -u root --password=root -e "CREATE DATABASE st19;"
mysql -u root --password=root -e "CREATE DATABASE st20;"
mysql -u root --password=root -e "CREATE DATABASE st21;"
mysql -u root --password=root -e "CREATE DATABASE st22;"
mysql -u root --password=root -e "CREATE DATABASE st23;"
mysql -u root --password=root -e "CREATE DATABASE st24;"
mysql -u root --password=root -e "CREATE DATABASE st25;"
mysql -u root --password=root -e "CREATE DATABASE st26;"
mysql -u root --password=root -e "CREATE DATABASE st27;"
mysql -u root --password=root -e "CREATE DATABASE st28;"
mysql -u root --password=root -e "CREATE DATABASE st29;"
mysql -u root --password=root -e "CREATE DATABASE st30;"
mysql -u root --password=root -e "CREATE DATABASE st31;"
mysql -u root --password=root -e "CREATE DATABASE st32;"
mysql -u root --password=root -e "CREATE DATABASE st33;"
mysql -u root --password=root -e "CREATE DATABASE st34;"
mysql -u root --password=root -e "CREATE DATABASE st35;"
mysql -u root --password=root -e "CREATE DATABASE st36;"
mysql -u root --password=root -e "CREATE DATABASE st37;"
mysql -u root --password=root -e "CREATE DATABASE st38;"
mysql -u root --password=root -e "CREATE DATABASE st39;"
mysql -u root --password=root -e "CREATE DATABASE st40;"
mysql -u root --password=root -e "CREATE DATABASE st41;"
mysql -u root --password=root -e "CREATE DATABASE st42;"
mysql -u root --password=root -e "CREATE DATABASE st43;"
mysql -u root --password=root -e "CREATE DATABASE st44;"
mysql -u root --password=root -e "CREATE DATABASE st45;"
mysql -u root --password=root -e "CREATE DATABASE st46;"
mysql -u root --password=root -e "CREATE DATABASE st47;"
mysql -u root --password=root -e "CREATE DATABASE st48;"
mysql -u root --password=root -e "CREATE DATABASE st49;"
mysql -u root --password=root -e "CREATE DATABASE st50;"
;;
postgresql)
service postgresql start
sudo -u postgres psql --command "CREATE USER root WITH SUPERUSER PASSWORD 'root';"
createdb
psql -c "create database supertokens;"
psql -c "create database st0;"
psql -c "create database st1;"
psql -c "create database st2;"
psql -c "create database st3;"
psql -c "create database st4;"
psql -c "create database st5;"
psql -c "create database st6;"
psql -c "create database st7;"
psql -c "create database st8;"
psql -c "create database st9;"
psql -c "create database st10;"
psql -c "create database st11;"
psql -c "create database st12;"
psql -c "create database st13;"
psql -c "create database st14;"
psql -c "create database st15;"
psql -c "create database st16;"
psql -c "create database st17;"
psql -c "create database st18;"
psql -c "create database st19;"
psql -c "create database st20;"
psql -c "create database st21;"
psql -c "create database st22;"
psql -c "create database st23;"
psql -c "create database st24;"
psql -c "create database st25;"
psql -c "create database st26;"
psql -c "create database st27;"
psql -c "create database st28;"
psql -c "create database st29;"
psql -c "create database st30;"
psql -c "create database st31;"
psql -c "create database st32;"
psql -c "create database st33;"
psql -c "create database st34;"
psql -c "create database st35;"
psql -c "create database st36;"
psql -c "create database st37;"
psql -c "create database st38;"
psql -c "create database st39;"
psql -c "create database st40;"
psql -c "create database st41;"
psql -c "create database st42;"
psql -c "create database st43;"
psql -c "create database st44;"
psql -c "create database st45;"
psql -c "create database st46;"
psql -c "create database st47;"
psql -c "create database st48;"
psql -c "create database st49;"
psql -c "create database st50;"
esac

View File

@ -1,8 +0,0 @@
case $1 in
mysql)
service mysql stop
;;
postgresql)
service postgresql stop
;;
esac

View File

@ -1,71 +0,0 @@
# inside supertokens downloaded zip
./install
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
supertokens start --port=8888
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
supertokens list
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
sed -i 's/# mysql_connection_uri:/mysql_connection_uri: "mysql:\/\/root:root@localhost:3306?rewriteBatchedStatements=true"/g' /usr/lib/supertokens/config.yaml
sed -i 's/# mongodb_connection_uri:/mongodb_connection_uri: mongodb:\/\/root:root@localhost:27017/g' /usr/lib/supertokens/config.yaml
sed -i 's/# disable_telemetry:/disable_telemetry: true/g' /usr/lib/supertokens/config.yaml
supertokens start --port=8889
supertokens list
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
curl http://localhost:8889/hello
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
curl http://localhost:8888/hello
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
supertokens stop
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi
supertokens uninstall
if [[ $? -ne 0 ]]
then
echo "cli testing failed... exiting!"
exit 1
fi

View File

@ -4,22 +4,20 @@ 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 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
# 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
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 'JAVA_HOME=/usr/java/jdk-15.0.1' >> /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-21.0.7/bin/java" 1
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-21.0.7/bin/javac" 1
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 wget -O docker-entrypoint.sh https://raw.githubusercontent.com/supertokens/supertokens-docker-postgresql/master/docker-entrypoint.sh
@ -53,7 +51,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-21.0.7-linux.zip" \
&& wget -O jre.zip "https://raw.githubusercontent.com/supertokens/jre/master/jre-15.0.1-linux.zip" \
&& mkdir -p /usr/lib/supertokens/jre \
&& unzip jre.zip \
&& mv jre-*/* /usr/lib/supertokens/jre \

View File

@ -65,4 +65,4 @@ register_plugin_version(
plugin_version=plugin_version,
plugin_interface_array=plugin_interface_array,
plugin_name=os.environ.get("PLUGIN_NAME")
)
)

View File

@ -45,16 +45,16 @@ jobs:
runs-on: ubuntu-latest
needs: dependency-branches
steps:
- name: Set up JDK 21.0.7
- name: Set up JDK 15.0.1
uses: actions/setup-java@v2
with:
java-version: 21.0.7
java-version: 15.0.1
distribution: zulu
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-root
path: ./supertokens-root
ref: master
ref: for_jdk_15_releases
- name: Checkout supertokens-core
run: |
cd supertokens-root

View File

@ -1,134 +0,0 @@
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 }}"

View File

@ -53,10 +53,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up JDK 21.0.7
- name: Set up JDK 15.0.1
uses: actions/setup-java@v2
with:
java-version: 21.0.7
java-version: 15.0.1
distribution: zulu
- name: Login to Docker Hub
uses: docker/login-action@v3
@ -81,16 +81,16 @@ jobs:
runs-on: ubuntu-latest
needs: [dependency-branches, release-docker]
steps:
- name: Set up JDK 21.0.7
- name: Set up JDK 15.0.1
uses: actions/setup-java@v2
with:
java-version: 21.0.7
java-version: 15.0.1
distribution: zulu
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-root
path: ./supertokens-root
ref: master
ref: for_jdk_15_releases
- name: Checkout supertokens-core
run: |
cd supertokens-root

View File

@ -8,20 +8,20 @@ jobs:
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
- 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'
- 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
uses: ./.github/workflows/unit-test.yml

View File

@ -19,8 +19,6 @@ jobs:
id: result
with:
run-for: PR
core-branch: ${{ github.ref_name }}
docker:
name: Docker
runs-on: ubuntu-latest
@ -36,16 +34,16 @@ jobs:
# - mysql
# - mongodb
steps:
- name: Set up JDK 21.0.7
- name: Set up JDK 15.0.1
uses: actions/setup-java@v2
with:
java-version: 21.0.7
java-version: 15.0.1
distribution: zulu
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-root
path: ./supertokens-root
ref: master
ref: for_jdk_15_releases
- uses: actions/checkout@v2
with:
path: ./supertokens-root/supertokens-core
@ -89,6 +87,7 @@ jobs:
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx

View File

@ -10,7 +10,7 @@ on:
jobs:
stress-tests:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
@ -44,4 +44,4 @@ jobs:
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
jq -r '.measurements[] | "| \(.title) | \(.formatted) |"' stress-tests/stats.json >> $GITHUB_STEP_SUMMARY

View File

@ -3,10 +3,13 @@ name: Unit Tests
on:
workflow_call:
env:
total-runners: 12
jobs:
dependency-branches:
name: Dependency Branches
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
outputs:
branches: ${{ steps.result.outputs.branches }}
@ -16,33 +19,45 @@ jobs:
id: result
with:
run-for: PR
core-branch: ${{ github.head_ref }}
test:
name: Unit tests
runner-indexes:
runs-on: ubuntu-latest
name: Generate runner indexes
needs: dependency-branches
outputs:
json: ${{ steps.generate-index-list.outputs.json }}
steps:
- id: generate-index-list
run: |
MAX_INDEX=$((${{ env.total-runners }}-1))
INDEX_LIST=$(seq 0 ${MAX_INDEX})
INDEX_JSON=$(jq --null-input --compact-output '. |= [inputs]' <<< ${INDEX_LIST})
echo "::set-output name=json::${INDEX_JSON}"
unit-tests:
runs-on: ubuntu-latest
name: "Unit tests: ${{ matrix.plugin }} plugin, runner #${{ matrix.runner-index }}"
needs:
- dependency-branches
- runner-indexes
strategy:
fail-fast: false
matrix:
runner-index: ${{ fromjson(needs.runner-indexes.outputs.json) }}
plugin:
- sqlite
- postgresql
# no longer supported
# - mysql
# - mongodb
runs-on: ubuntu-22.04
steps:
- name: Set up JDK 21.0.7
- name: Set up JDK 15.0.1
uses: actions/setup-java@v2
with:
java-version: 21.0.7
java-version: 15.0.1
distribution: zulu
- uses: actions/checkout@v2
with:
repository: supertokens/supertokens-root
path: ./supertokens-root
ref: master
ref: for_jdk_15_releases
- uses: actions/checkout@v2
with:
path: ./supertokens-root/supertokens-core
@ -71,12 +86,36 @@ jobs:
- name: Start ${{ matrix.plugin }} server
if: matrix.plugin != 'sqlite'
run: cd supertokens-root/supertokens-${{ matrix.plugin }}-plugin && ./startDb.sh
- name: Start oauth provider
run: |
docker run -d -p 4444:4444 -p 4445:4445 rishabhpoddar/oauth-server-cicd
- uses: chaosaffe/split-tests@v1-alpha.1
id: split-tests
name: Split tests
with:
glob: 'supertokens-root/*/src/test/java/**/*.java'
split-total: ${{ env.total-runners }}
split-index: ${{ matrix.runner-index }}
- run: 'echo "This runner will execute the following tests: ${{ steps.split-tests.outputs.test-suite }}"'
- name: Run tests
env:
ST_PLUGIN_NAME: ${{ matrix.plugin }}
run: |
cd supertokens-root
./gradlew test
echo "./gradlew test \\" > test.sh
chmod +x test.sh
IFS=' ' read -ra TESTS <<< "${{ steps.split-tests.outputs.test-suite }}"
for test in "${TESTS[@]}"; do
test_name="${test%.java}"
test_name="${test_name#supertokens-root/supertokens-core/src/test/java/}"
test_name="${test_name//\//.}"
echo " --tests $test_name \\" >> test.sh
done
echo "" >> test.sh
echo "this is the test command:"
cat test.sh
echo "--------------------------------"
./test.sh
- name: Publish Test Report
uses: mikepenz/action-junit-report@v5
if: always()

6
.gitignore vendored
View File

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

View File

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

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 21.0.7. Installation instructions for Mac and Linux can be found
- JDK: openjdk 15.0.1. 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/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/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/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,8 +8,6 @@
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" }
@ -21,37 +19,27 @@ compileTestJava { options.encoding = "UTF-8" }
// }
//}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
version = "11.3.0"
version = "10.0.4"
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.13.1'
implementation group: 'com.google.code.gson', name: 'gson', version: '2.3.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'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-cbor
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-cbor', version: '2.18.2'
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.2'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1'
// https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core
api group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '11.0.12'
implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18'
// https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305
implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
@ -83,18 +71,9 @@ dependencies {
// https://mvnrepository.com/artifact/com.googlecode.libphonenumber/libphonenumber/
implementation group: 'com.googlecode.libphonenumber', name: 'libphonenumber', version: '8.13.25'
// https://mvnrepository.com/artifact/com.webauthn4j/webauthn4j-core
implementation group: 'com.webauthn4j', name: 'webauthn4j-core', version: '0.28.6.RELEASE'
implementation platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha:2.17.0-alpha")
// Open SAML
implementation group: 'org.opensaml', name: 'opensaml-core', version: '4.3.1'
implementation group: 'org.opensaml', name: 'opensaml-saml-impl', version: '4.3.1'
implementation group: 'org.opensaml', name: 'opensaml-security-impl', version: '4.3.1'
implementation group: 'org.opensaml', name: 'opensaml-profile-impl', version: '4.3.1'
implementation group: 'org.opensaml', name: 'opensaml-xmlsec-impl', version: '4.3.1'
implementation("ch.qos.logback:logback-core:1.5.18")
implementation("ch.qos.logback:logback-classic:1.5.18")
@ -106,13 +85,10 @@ dependencies {
implementation("io.opentelemetry.semconv:opentelemetry-semconv")
implementation('org.aspectj:aspectjrt:1.9.24')
compileOnly project(":supertokens-plugin-interface")
testImplementation project(":supertokens-plugin-interface")
// this is so that we can find plugin-interface jar while testing
testImplementation project(":supertokens-plugin-interface")
testImplementation 'junit:junit:4.12'
// https://mvnrepository.com/artifact/org.mockito/mockito-core
@ -123,9 +99,8 @@ dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit4:0.22.0'
// https://mvnrepository.com/artifact/com.webauthn4j/webauthn4j-test
testImplementation group: 'com.webauthn4j', name: 'webauthn4j-test', version: '0.28.6.RELEASE'
}
application {
mainClass.set("io.supertokens.Main")
}
@ -135,47 +110,43 @@ jar {
}
tasks.register('copyJars', Copy) {
task copyJars(type: Copy) {
into "$buildDir/dependencies"
from configurations.runtimeClasspath
into layout.buildDirectory.dir("dependencies")
}
test {
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"]
jvmArgs '-Djava.security.egd=file:/dev/urandom'
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).configureEach {
tasks.withType(Test) {
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,8 +4,6 @@ plugins {
repositories {
mavenCentral()
maven { url 'https://build.shibboleth.net/nexus/content/repositories/releases/' }
}
application {
@ -18,13 +16,13 @@ jar {
dependencies {
// https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation group: 'com.google.code.gson', name: 'gson', version: '2.13.1'
implementation group: 'com.google.code.gson', name: 'gson', version: '2.3.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'
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.2'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1'
// https://mvnrepository.com/artifact/de.mkammerer/argon2-jvm
implementation group: 'de.mkammerer', name: 'argon2-jvm', version: '2.11'
@ -35,9 +33,9 @@ dependencies {
testImplementation group: 'junit', name: 'junit', version: '4.12'
}
tasks.register('copyJars', Copy) {
task copyJars(type: Copy) {
into "$buildDir/dependencies"
from configurations.runtimeClasspath
into layout.buildDirectory.dir("dependencies")
}
test {
@ -57,10 +55,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 {
@ -70,7 +68,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,40 +1,55 @@
{
"_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"
}
]
"_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"
}
]
}

Binary file not shown.

View File

@ -43,38 +43,12 @@ 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");
}
@ -103,27 +77,6 @@ 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");
}
@ -148,7 +101,6 @@ 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());
@ -229,13 +181,6 @@ 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

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

View File

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

View File

@ -175,29 +175,6 @@ disable_telemetry: true
# the supertokens core will use the specified number of threads to complete the migration of users.
# bulk_migration_parallelism:
# (DIFFERENT_ACROSS_APPS | OPTIONAL | Default: 8000) int value. If specified, the supertokens core will load the
# specified number of users for migrating in one single batch.
# bulk_migration_batch_size:
# (DIFFERENT_ACROSS_APPS | OPTIONAL | Default: 3600000) long value. Time in milliseconds for how long a webauthn
# account recovery token is valid for.
# webauthn_recover_account_token_lifetime:
# (OPTIONAL | Default: null) string value. The URL of the OpenTelemetry collector to which the core
# (OPTIONAL | Default: http://localhost:4317) 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'
}
tasks.register('copyJars', Copy) {
task copyJars(type: Copy) {
into "$buildDir/dependencies"
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,7 +14,6 @@ exitIfNeeded
exitIfNeeded
(cd ../../ && ./gradlew :$prefix-core:downloader:copyJars < /dev/null)
exitIfNeeded

View File

@ -2,12 +2,10 @@ plugins {
id 'java-library'
}
version = 'unspecified'
version 'unspecified'
repositories {
mavenCentral()
maven { url 'https://build.shibboleth.net/nexus/content/repositories/releases/' }
}
jar {
@ -15,7 +13,7 @@ jar {
}
dependencies {
compileOnly group: 'com.google.code.gson', name: 'gson', version: '2.13.1'
compileOnly group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
compileOnly project(":supertokens-plugin-interface")
testImplementation project(":supertokens-plugin-interface")
@ -37,13 +35,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: '11.0.5'
testImplementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18'
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.13'
testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14'
// https://mvnrepository.com/artifact/com.google.code.gson/gson
testImplementation group: 'com.google.code.gson', name: 'gson', version: '2.13.1'
testImplementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
testImplementation 'com.tngtech.archunit:archunit-junit4:0.22.0'
@ -54,18 +52,17 @@ dependencies {
testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1'
testImplementation group: 'org.jetbrains', name: 'annotations', version: '13.0'
}
tasks.register('copyJars', Copy) {
task copyJars(type: Copy) {
into "$buildDir/dependencies"
from configurations.runtimeClasspath
into layout.buildDirectory.dir("dependencies")
}
def interfaceName = "io.supertokens.featureflag.EEFeatureFlagInterface"
def className = "io.supertokens.ee.EEFeatureFlag"
tasks.register('generateMetaInf') {
task generateMetaInf {
doFirst {
mkdir "src/main/resources/META-INF/services"
file("src/main/resources/META-INF/services/${interfaceName}").text = "${className}"
@ -92,10 +89,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 {
@ -105,7 +102,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,7 +14,6 @@ exitIfNeeded
exitIfNeeded
(cd ../../ && ./gradlew :$prefix-core:ee:copyJars < /dev/null)
exitIfNeeded

View File

@ -34,7 +34,6 @@ 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;
@ -387,34 +386,6 @@ 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();
@ -462,10 +433,6 @@ 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());
@ -556,7 +523,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.com/0/st/license/check",
"https://api.supertokens.io/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.getProcess()).setIntervalInSeconds(EELicenseCheck.RESOURCE_KEY, 1);
CronTaskTest.getInstance(process.main).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.getProcess())) {
if (StorageLayer.isInMemDb(process.main)) {
// 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.getProcess().deleteAllInformationForTesting();
process.main.deleteAllInformationForTesting();
process.kill();
System.out.println("----------DELETE ALL INFORMATION----------");
}

View File

@ -24,8 +24,7 @@ public abstract class Utils extends Mockito {
try {
// remove config.yaml file
String workerId = System.getProperty("org.gradle.test.worker", "");
ProcessBuilder pb = new ProcessBuilder("rm", "config" + workerId + ".yaml");
ProcessBuilder pb = new ProcessBuilder("rm", "config.yaml");
pb.directory(new File(installDir));
Process process = pb.start();
process.waitFor();
@ -59,8 +58,7 @@ 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");
String workerId = System.getProperty("org.gradle.test.worker", "");
File currentConfig = new File("../../config" + workerId + ".yaml");
File currentConfig = new File("../../config.yaml");
if (currentConfig.isFile()) {
byte[] ogConfigContent = Files.readAllBytes(ogConfig.toPath());
byte[] currentConfigContent = Files.readAllBytes(currentConfig.toPath());
@ -69,7 +67,7 @@ public abstract class Utils extends Mockito {
}
}
ProcessBuilder pb = new ProcessBuilder("cp", "temp/config.yaml", "./config" + workerId + ".yaml");
ProcessBuilder pb = new ProcessBuilder("cp", "temp/config.yaml", "./config.yaml");
pb.directory(new File(installDir));
Process process = pb.start();
process.waitFor();
@ -98,15 +96,14 @@ public abstract class Utils extends Mockito {
String newStr = "\n# " + key + ":";
StringBuilder originalFileContent = new StringBuilder();
String workerId = System.getProperty("org.gradle.test.worker", "");
try (BufferedReader reader = new BufferedReader(new FileReader("../../config" + workerId + ".yaml"))) {
try (BufferedReader reader = new BufferedReader(new FileReader("../../config.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" + workerId + ".yaml"))) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("../../config.yaml"))) {
writer.write(modifiedFileContent);
}
}
@ -120,15 +117,14 @@ public abstract class Utils extends Mockito {
String oldStr = "\n((#\\s)?)" + key + "(:|((:\\s).+))\n";
String newStr = "\n" + key + ": " + value + "\n";
StringBuilder originalFileContent = new StringBuilder();
String workerId = System.getProperty("org.gradle.test.worker", "");
try (BufferedReader reader = new BufferedReader(new FileReader("../../config" + workerId + ".yaml"))) {
try (BufferedReader reader = new BufferedReader(new FileReader("../../config.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" + workerId + ".yaml"))) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("../../config.yaml"))) {
writer.write(modifiedFileContent);
}
}

View File

@ -45,7 +45,7 @@ public class DeleteLicenseKeyAPITest {
// check that no LicenseKey exits
try {
FeatureFlag.getInstance(process.getProcess()).getLicenseKey();
FeatureFlag.getInstance(process.main).getLicenseKey();
fail();
} catch (NoLicenseKeyFoundException ignored) {
}
@ -58,7 +58,7 @@ public class DeleteLicenseKeyAPITest {
// check that no LicenseKey exits
try {
FeatureFlag.getInstance(process.getProcess()).getLicenseKey();
FeatureFlag.getInstance(process.main).getLicenseKey();
fail();
} catch (NoLicenseKeyFoundException ignored) {
}
@ -90,7 +90,7 @@ public class DeleteLicenseKeyAPITest {
// check that no LicenseKey exits
try {
FeatureFlag.getInstance(process.getProcess()).getLicenseKey();
FeatureFlag.getInstance(process.main).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.getProcess())) {
if (StorageLayer.isInMemDb(process.main)) {
// 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.getProcess())) {
if (StorageLayer.isInMemDb(process.main)) {
// 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.getProcess()).getEeFeatureFlagInstance());
Assert.assertNull(FeatureFlag.getInstance(process.main).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.getProcess()).getEeFeatureFlagInstance());
Assert.assertNull(FeatureFlag.getInstance(process.main).getEeFeatureFlagInstance());
Assert.assertEquals(0, FeatureFlag.getInstance(process.getProcess()).getEnabledFeatures().length);
Assert.assertEquals(FeatureFlag.getInstance(process.getProcess()).getEnabledFeatures().length, 0);
// set license key when ee folder does not exist
JsonObject requestBody = new JsonObject();

View File

@ -2,44 +2,34 @@
"_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/com/google/code/gson/gson/2.3.1/gson-2.3.1.jar",
"name":"gson 2.3.1",
"src":"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1-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/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1.jar",
"name":"jackson-dataformat-yaml 2.16.1",
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1-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/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar",
"name":"snakeyaml 2.2",
"src":"https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2-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/core/jackson-databind/2.16.1/jackson-databind-2.16.1.jar",
"name":"jackson-databind 2.16.1",
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1-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/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18.jar",
"name":"tomcat-embed-core 10.1.18",
"src":"https://repo.maven.apache.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18-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/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://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.18/tomcat-annotations-api-10.1.18-sources.jar"
},
{
"jar":"https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar",
@ -96,151 +86,6 @@
"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",
@ -251,11 +96,6 @@
"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",

BIN
jar/core-10.0.3.jar Normal file

Binary file not shown.

Binary file not shown.

View File

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

View File

@ -24,7 +24,7 @@ public class ActiveUsers {
@TestOnly
public static void updateLastActive(Main main, String userId) {
try {
ActiveUsers.updateLastActive(ResourceDistributor.getAppForTesting().toAppIdentifier(),
ActiveUsers.updateLastActive(new AppIdentifier(null, null),
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, ResourceDistributor.getAppForTesting().toAppIdentifier(), time);
return countUsersActiveSince(main, new AppIdentifier(null, null), time);
}
}

View File

@ -22,9 +22,6 @@ 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;
@ -44,7 +41,6 @@ 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;
@ -95,9 +91,6 @@ public class Main {
private boolean waitToEnableFeatureFlag = false;
private final Object waitToEnableFeatureFlagLock = new Object();
//setting to true by default
private final Boolean bulkMigrationCronEnabled = System.getenv("BULK_MIGRATION_CRON_ENABLED") == null || Boolean.parseBoolean(System.getenv("BULK_MIGRATION_CRON_ENABLED"));
private boolean forceInMemoryDB = false;
@ -124,8 +117,6 @@ 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;
}
@ -160,12 +151,9 @@ 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);
}
@ -173,11 +161,14 @@ 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, Config.getBaseConfigAsJsonObject(this));
StorageLayer.initPrimary(this, CLIOptions.get(this).getInstallationPath() + "plugin/",
Config.getBaseConfigAsJsonObject(this));
} catch (InvalidConfigException e) {
throw new QuitProgramException(e);
}
@ -185,9 +176,6 @@ public class Main {
// init file logging
Logging.initFileLogging(this);
// Required for SAML related stuff
SAMLBootstrap.initialize();
// initialise cron job handler
Cronjobs.init(this);
@ -276,21 +264,10 @@ public class Main {
Cronjobs.addCronjob(this, DeleteExpiredAccessTokenSigningKeys.init(this, uniqueUserPoolIdsTenants));
// initializes ProcessBulkImportUsers cronjob to process bulk import users
if(bulkMigrationCronEnabled) {
Cronjobs.addCronjob(this, ProcessBulkImportUsers.init(this, uniqueUserPoolIdsTenants));
}
Cronjobs.addCronjob(this, ProcessBulkImportUsers.init(this, uniqueUserPoolIdsTenants));
Cronjobs.addCronjob(this, CleanupOAuthSessionsAndChallenges.init(this, uniqueUserPoolIdsTenants));
Cronjobs.addCronjob(this, CleanUpWebauthNExpiredDataCron.init(this, uniqueUserPoolIdsTenants));
// starts the DeadlockLogger if
if (Config.getBaseConfig(this).isDeadlockLoggerEnabled()) {
DeadlockLogger.getInstance().start();
}
Cronjobs.addCronjob(this, DeleteExpiredSAMLData.init(this, uniqueUserPoolIdsTenants));
// this is to ensure tenantInfos are in sync for the new cron job as well
MultitenancyHelper.getInstance(this).refreshCronjobs();
@ -380,16 +357,12 @@ 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 + startedDir + "\\" + config.getHost(this) + "-"
? fileLocation + ".started\\" + config.getHost(this) + "-"
+ config.getPort(this)
: fileLocation + startedDir + "/" + config.getHost(this) + "-"
: fileLocation + ".started/" + config.getHost(this) + "-"
+ config.getPort(this);
File dotStarted = new File(fileName);
if (!dotStarted.exists()) {
@ -432,10 +405,9 @@ public class Main {
@TestOnly
public void killForTestingAndWaitForShutdown() throws InterruptedException {
// Do not kill for now
assertIsTesting();
wakeUpMainThreadToShutdown();
mainThread.join();
assertIsTesting();
wakeUpMainThreadToShutdown();
mainThread.join();
}
// must not throw any error

View File

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

View File

@ -35,28 +35,16 @@ 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;
}
@TestOnly
public static void setAppForTesting(TenantIdentifier app) {
appUsedForTesting = app;
}
@TestOnly
public static TenantIdentifier getAppForTesting() {
return appUsedForTesting;
}
public SingletonResource getResource(AppIdentifier appIdentifier, @Nonnull String key)
public synchronized SingletonResource getResource(AppIdentifier appIdentifier, @Nonnull String key)
throws TenantOrAppNotFoundException {
return getResource(appIdentifier.getAsPublicTenantIdentifier(), key);
}
public SingletonResource getResource(TenantIdentifier tenantIdentifier, @Nonnull String key)
public synchronized SingletonResource getResource(TenantIdentifier tenantIdentifier, @Nonnull String key)
throws TenantOrAppNotFoundException {
// first we do exact match
SingletonResource resource = resources.get(new KeyClass(tenantIdentifier, key));
@ -70,6 +58,14 @@ 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()) {
@ -93,11 +89,11 @@ public class ResourceDistributor {
}
@TestOnly
public SingletonResource getResource(@Nonnull String key) {
return resources.get(new KeyClass(appUsedForTesting, key));
public synchronized SingletonResource getResource(@Nonnull String key) {
return resources.get(new KeyClass(new TenantIdentifier(null, null, null), key));
}
public SingletonResource setResource(TenantIdentifier tenantIdentifier,
public synchronized SingletonResource setResource(TenantIdentifier tenantIdentifier,
@Nonnull String key,
SingletonResource resource) {
SingletonResource alreadyExists = resources.get(new KeyClass(tenantIdentifier, key));
@ -108,7 +104,7 @@ public class ResourceDistributor {
return resource;
}
public SingletonResource removeResource(TenantIdentifier tenantIdentifier,
public synchronized SingletonResource removeResource(TenantIdentifier tenantIdentifier,
@Nonnull String key) {
SingletonResource singletonResource = resources.get(new KeyClass(tenantIdentifier, key));
if (singletonResource == null) {
@ -118,18 +114,18 @@ public class ResourceDistributor {
return singletonResource;
}
public SingletonResource setResource(AppIdentifier appIdentifier,
public synchronized SingletonResource setResource(AppIdentifier appIdentifier,
@Nonnull String key,
SingletonResource resource) {
return setResource(appIdentifier.getAsPublicTenantIdentifier(), key, resource);
}
public SingletonResource removeResource(AppIdentifier appIdentifier,
public synchronized SingletonResource removeResource(AppIdentifier appIdentifier,
@Nonnull String key) {
return removeResource(appIdentifier.getAsPublicTenantIdentifier(), key);
}
public void clearAllResourcesWithResourceKey(String inputKey) {
public synchronized void clearAllResourcesWithResourceKey(String inputKey) {
List<KeyClass> toRemove = new ArrayList<>();
resources.forEach((key, value) -> {
if (key.key.equals(inputKey)) {
@ -141,7 +137,7 @@ public class ResourceDistributor {
}
}
public Map<KeyClass, SingletonResource> getAllResourcesWithResourceKey(String inputKey) {
public synchronized Map<KeyClass, SingletonResource> getAllResourcesWithResourceKey(String inputKey) {
Map<KeyClass, SingletonResource> result = new HashMap<>();
resources.forEach((key, value) -> {
if (key.key.equals(inputKey)) {
@ -152,9 +148,9 @@ public class ResourceDistributor {
}
@TestOnly
public SingletonResource setResource(@Nonnull String key,
public synchronized SingletonResource setResource(@Nonnull String key,
SingletonResource resource) {
return setResource(appUsedForTesting, key, resource);
return setResource(new TenantIdentifier(null, null, null), key, resource);
}
public interface Func<T> {

View File

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

View File

@ -1,27 +0,0 @@
/*
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.supertokens.authRecipe.exception;
public class BulkImportRecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException extends Exception {
public final String recipeUserId;
public BulkImportRecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(String recipeUserId) {
super("The recipe user id '" + recipeUserId + "' is already linked with another primary user id");
this.recipeUserId = recipeUserId;
}
}

View File

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

View File

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

View File

@ -52,8 +52,6 @@ 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;
@ -93,20 +91,12 @@ 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);
JsonObject configJson = new GsonBuilder().serializeNulls().create().toJsonTree(obj).getAsJsonObject();
CoreConfig.updateConfigJsonFromEnv(configJson);
StorageLayer.updateConfigJsonFromEnv(main, configJson);
return configJson;
return new GsonBuilder().serializeNulls().create().toJsonTree(obj).getAsJsonObject();
}
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() + configFile
? CLIOptions.get(main).getInstallationPath() + "config.yaml"
: CLIOptions.get(main).getConfigFilePath();
}
@ -315,7 +305,7 @@ public class Config extends ResourceDistributor.SingletonResource {
@TestOnly
public static CoreConfig getConfig(Main main) {
try {
return getConfig(ResourceDistributor.getAppForTesting(), main);
return getConfig(new TenantIdentifier(null, null, null), main);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}

View File

@ -67,8 +67,7 @@ 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",
"saml_legacy_acs_url"
"oauth_provider_url_configured_in_oauth_provider"
};
@IgnoreForAnnotationCheck
@ -76,13 +75,11 @@ 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(
@ -91,20 +88,17 @@ 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(
@ -112,27 +106,23 @@ 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(
@ -143,7 +133,6 @@ public class CoreConfig {
@IgnoreForAnnotationCheck
private final String logDefault = "asdkfahbdfk3kjHS";
@EnvName("INFO_LOG_PATH")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
@ -152,7 +141,6 @@ public class CoreConfig {
"directory/logs/info.log)")
private String info_log_path = logDefault;
@EnvName("ERROR_LOG_PATH")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
@ -161,7 +149,6 @@ public class CoreConfig {
"directory/logs/error.log)")
private String error_log_path = logDefault;
@EnvName("ACCESS_TOKEN_SIGNING_KEY_DYNAMIC")
@NotConflictingInApp
@JsonProperty
@ConfigDescription(
@ -169,20 +156,17 @@ 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(
@ -190,13 +174,11 @@ 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
@ -206,7 +188,6 @@ public class CoreConfig {
"length of 20 chars. (Default: null)")
private String api_keys = null;
@EnvName("DISABLE_TELEMETRY")
@NotConflictingInApp
@JsonProperty
@ConfigDescription(
@ -214,32 +195,27 @@ 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(
@ -247,7 +223,6 @@ public class CoreConfig {
"(Default: 1)")
private int argon2_hashing_pool_size = 1;
@EnvName("FIREBASE_PASSWORD_HASHING_POOL_SIZE")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
@ -255,7 +230,6 @@ 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)")
@ -271,16 +245,13 @@ 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(
@ -289,13 +260,11 @@ 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(
@ -303,7 +272,6 @@ 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(
@ -311,7 +279,6 @@ public class CoreConfig {
" address.")
private String ip_deny_regex = null;
@EnvName("OAUTH_PROVIDER_PUBLIC_SERVICE_URL")
@NotConflictingInApp
@JsonProperty
@HideFromDashboard
@ -319,7 +286,6 @@ 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
@ -327,7 +293,6 @@ 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
@ -335,7 +300,6 @@ 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
@ -343,14 +307,12 @@ 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(
@ -360,7 +322,6 @@ public class CoreConfig {
"regular api_keys config.")
private String supertokens_saas_secret = null;
@EnvName("SUPERTOKENS_MAX_CDI_VERSION")
@NotConflictingInApp
@JsonProperty
@HideFromDashboard
@ -370,7 +331,6 @@ public class CoreConfig {
"null)")
private String supertokens_max_cdi_version = null;
@EnvName("SUPERTOKENS_SAAS_LOAD_ONLY_CUD")
@ConfigYamlOnly
@JsonProperty
@ConfigDescription(
@ -378,73 +338,18 @@ 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;
@ -456,6 +361,15 @@ public class CoreConfig {
disableOAuthValidationForTest = val;
}
@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: " +
"http://localhost:4317)")
private String otel_collector_connection_uri = "http://localhost:4317";
public static Set<String> getValidFields() {
CoreConfig coreConfig = new CoreConfig();
JsonObject coreConfigObj = new GsonBuilder().serializeNulls().create().toJsonTree(coreConfig).getAsJsonObject();
@ -513,10 +427,6 @@ 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;
@ -688,84 +598,16 @@ public class CoreConfig {
return bulk_migration_parallelism;
}
public long getWebauthnRecoverAccountTokenLifetime() {
return webauthn_recover_account_token_lifetime;
}
public int getBulkMigrationBatchSize() {
return bulk_migration_batch_size;
}
public String getOtelCollectorConnectionURI() {
return otel_collector_connection_uri;
}
public boolean isDeadlockLoggerEnabled() {
return deadlock_logger_enable;
}
public String getSAMLLegacyACSURL() {
return saml_legacy_acs_url;
}
public String getSAMLSPEntityID() {
return saml_sp_entity_id;
}
public long getSAMLClaimsValidity() {
return saml_claims_validity;
}
public long getSAMLRelayStateValidity() {
return saml_relay_state_validity;
}
private String getConfigFileLocation(Main main) {
return new File(CLIOptions.get(main).getConfigFilePath() == null
? CLIOptions.get(main).getInstallationPath() + "config.yaml"
: CLIOptions.get(main).getConfigFilePath()).getAbsolutePath();
}
public static void updateConfigJsonFromEnv(JsonObject configJson) {
Map<String, String> env = System.getenv();
for (Field field : CoreConfig.class.getDeclaredFields()) {
if (field.isAnnotationPresent(EnvName.class)) {
String envName = field.getAnnotation(EnvName.class).value();
String stringValue = env.get(envName);
if (stringValue == null || stringValue.isEmpty()) {
continue;
}
if (stringValue.startsWith("\"") && stringValue.endsWith("\"")) {
stringValue = stringValue.substring(1, stringValue.length() - 1);
stringValue = stringValue
.replace("\\n", "\n")
.replace("\\t", "\t")
.replace("\\r", "\r")
.replace("\\\"", "\"")
.replace("\\'", "'")
.replace("\\\\", "\\");
}
if (field.getType().equals(String.class)) {
configJson.addProperty(field.getName(), stringValue);
} else if (field.getType().equals(int.class)) {
configJson.addProperty(field.getName(), Integer.parseInt(stringValue));
} else if (field.getType().equals(long.class)) {
configJson.addProperty(field.getName(), Long.parseLong(stringValue));
} else if (field.getType().equals(boolean.class)) {
configJson.addProperty(field.getName(), Boolean.parseBoolean(stringValue));
} else if (field.getType().equals(float.class)) {
configJson.addProperty(field.getName(), Float.parseFloat(stringValue));
} else if (field.getType().equals(double.class)) {
configJson.addProperty(field.getName(), Double.parseDouble(stringValue));
}
}
}
}
void normalizeAndValidate(Main main, boolean includeConfigFilePath) throws InvalidConfigException {
if (isNormalizedAndValid) {
return;
@ -957,14 +799,6 @@ public class CoreConfig {
throw new InvalidConfigException("Provided bulk_migration_parallelism must be >= 1");
}
if (bulk_migration_batch_size < 1) {
throw new InvalidConfigException("Provided bulk_migration_batch_size must be >= 1");
}
if (webauthn_recover_account_token_lifetime <= 0) {
throw new InvalidConfigException("Provided webauthn_recover_account_token_lifetime must be > 0");
}
for (String fieldId : CoreConfig.getValidFields()) {
try {
Field field = CoreConfig.class.getDeclaredField(fieldId);
@ -988,10 +822,6 @@ 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("")) {
@ -1129,24 +959,6 @@ 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

@ -1,29 +0,0 @@
/*
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.supertokens.config.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
// Make annotation accessible at runtime so that config can be read from env
@Target(ElementType.FIELD) // Annotation can only be applied to fields
public @interface EnvName {
String value(); // String value that provides a env var name for the field
}

View File

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

View File

@ -77,8 +77,6 @@ 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<>();
@ -94,7 +92,7 @@ public class ProcessBulkUsersImportWorker implements Runnable {
// Validate the user
try {
validUsers.add(bulkImportUserUtils.createBulkImportUserFromJSON(main, appIdentifier,
user.toJsonObject(), BulkImportUserUtils.IDMode.READ_STORED));
user.toJsonObject(), user.id));
} catch (InvalidBulkImportDataException exception) {
validationErrorsBeforeActualProcessing.put(user.id, new Exception(
String.valueOf(exception.errors)));
@ -109,14 +107,12 @@ public class ProcessBulkUsersImportWorker implements Runnable {
// Since all the tenants of a user must share the storage, we will just use the
// storage of the first tenantId of the first loginMethod
Map<SQLStorage, List<BulkImportUser>> partitionedUsers = partitionUsersByStorage(appIdentifier, validUsers);
for(SQLStorage bulkImportProxyStorage : partitionedUsers.keySet()) {
boolean shouldRetryImmediatley = true;
while (shouldRetryImmediatley) {
shouldRetryImmediatley = bulkImportProxyStorage.startTransaction(con -> {
try {
BulkImport.processUsersImportSteps(main, appIdentifier, bulkImportProxyStorage,
partitionedUsers.get(bulkImportProxyStorage),
BulkImport.processUsersImportSteps(main, appIdentifier, bulkImportProxyStorage, partitionedUsers.get(bulkImportProxyStorage),
allStoragesForApp);
bulkImportProxyStorage.commitTransactionForBulkImportProxyStorage();
@ -126,18 +122,8 @@ public class ProcessBulkUsersImportWorker implements Runnable {
toDelete[i] = validUsers.get(i).id;
}
while (true){
try {
List<String> deletedIds = baseTenantStorage.deleteBulkImportUsers(appIdentifier,
toDelete);
break;
} catch (Exception e) {
// ignore and retry delete. The import transaction is already committed, the delete should happen no matter what
Logging.debug(main, app.getAsPublicTenantIdentifier(),
"Exception while deleting bulk import users: " + e.getMessage());
}
}
} catch (StorageTransactionLogicException | StorageQueryException e) {
baseTenantStorage.deleteBulkImportUsers(appIdentifier, toDelete);
} catch (StorageTransactionLogicException e) {
// We need to rollback the transaction manually because we have overridden that in the proxy
// storage
bulkImportProxyStorage.rollbackTransactionForBulkImportProxyStorage();
@ -152,15 +138,9 @@ 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
}
@ -183,40 +163,30 @@ public class ProcessBulkUsersImportWorker implements Runnable {
String[] errorMessage = { e.getMessage() };
Map<String, String> bulkImportUserIdToErrorMessage = new HashMap<>();
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]);
}
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]);
}
}
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);
}
} 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);
}
try {
@ -235,7 +205,7 @@ public class ProcessBulkUsersImportWorker implements Runnable {
Map<String, Exception> userIndexToError = exception.exceptionByUserId;
for(String userid : userIndexToError.keySet()){
Optional<BulkImportUser> userWithId = usersBatch.stream()
.filter(bulkImportUser -> userid.equals(bulkImportUser.id) || userid.equals(bulkImportUser.externalUserId)).findFirst();
.filter(bulkImportUser -> bulkImportUser.id.equals(userid) || bulkImportUser.externalUserId.equals(userid)).findFirst();
String id = null;
if(userWithId.isPresent()){
id = userWithId.get().id;
@ -314,7 +284,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.getFirst().tenantIds.getFirst());
appIdentifier.getAppId(), user.loginMethods.get(0).tenantIds.get(0));
SQLStorage bulkImportProxyStorage = (SQLStorage) getBulkImportProxyStorage(firstTenantIdentifier);
if(!result.containsKey(bulkImportProxyStorage)){

View File

@ -1,76 +0,0 @@
/*
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.supertokens.cronjobs.cleanupWebauthnExpiredData;
import io.supertokens.Main;
import io.supertokens.cronjobs.CronTask;
import io.supertokens.cronjobs.CronTaskTest;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.StorageUtils;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.webauthn.WebAuthNStorage;
import java.util.List;
public class CleanUpWebauthNExpiredDataCron extends CronTask {
public static final String RESOURCE_KEY = "io.supertokens.cronjobs.cleanupWebauthnExpiredData" +
".CleanUpWebauthnExpiredDataCron";
private CleanUpWebauthNExpiredDataCron(Main main, List<List<TenantIdentifier>> tenantsInfo) {
super("CleanUpWebauthnExpiredDataCron", main, tenantsInfo, true);
}
public static CleanUpWebauthNExpiredDataCron init(Main main, List<List<TenantIdentifier>> tenantsInfo) {
return (CleanUpWebauthNExpiredDataCron) main.getResourceDistributor()
.setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY,
new CleanUpWebauthNExpiredDataCron(main, tenantsInfo));
}
@Override
protected void doTaskPerStorage(Storage storage) throws Exception {
if (storage.getType() != STORAGE_TYPE.SQL) {
return;
}
WebAuthNStorage webAuthNStorage = StorageUtils.getWebAuthNStorage(storage);
webAuthNStorage.deleteExpiredAccountRecoveryTokens();
webAuthNStorage.deleteExpiredGeneratedOptions();
}
@Override
public int getIntervalTimeSeconds() {
if (Main.isTesting) {
Integer interval = CronTaskTest.getInstance(main).getIntervalInSeconds(RESOURCE_KEY);
if (interval != null) {
return interval;
}
}
// Every 24 hours.
return 24 * 3600;
}
@Override
public int getInitialWaitTimeSeconds() {
if (!Main.isTesting) {
return getIntervalTimeSeconds();
} else {
return 0;
}
}
}

View File

@ -1,84 +0,0 @@
/*
* 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

@ -1,53 +0,0 @@
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,7 +7,6 @@ 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;
@ -31,11 +30,7 @@ public class DeleteExpiredTotpTokens extends CronTask {
@TestOnly
public static DeleteExpiredTotpTokens getInstance(Main main) {
try {
return (DeleteExpiredTotpTokens) main.getResourceDistributor().getResource(TenantIdentifier.BASE_TENANT, RESOURCE_KEY);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
return (DeleteExpiredTotpTokens) main.getResourceDistributor().getResource(RESOURCE_KEY);
}
@Override

View File

@ -19,9 +19,12 @@ 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 io.supertokens.pluginInterface.opentelemetry.WithinOtelSpan;
import java.util.List;
public class SyncCoreConfigWithDb extends CronTask {
@ -59,7 +62,6 @@ 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.com/0/st/telemetry";
String url = "https://api.supertokens.io/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,7 +17,6 @@
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;
@ -56,7 +55,7 @@ public class Dashboard {
throws StorageQueryException, DuplicateEmailException, FeatureNotEnabledException {
try {
Storage storage = StorageLayer.getStorage(main);
return signUpDashboardUser(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
return signUpDashboardUser(new AppIdentifier(null, null), storage,
main, email, password);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -104,7 +103,7 @@ public class Dashboard {
public static DashboardUser[] getAllDashboardUsers(Main main)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getAllDashboardUsers(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, main);
return getAllDashboardUsers(new AppIdentifier(null, null), storage, main);
}
public static DashboardUser[] getAllDashboardUsers(AppIdentifier appIdentifier, Storage storage, Main main)
@ -128,7 +127,7 @@ public class Dashboard {
throws StorageQueryException, UserSuspendedException {
try {
Storage storage = StorageLayer.getStorage(main);
return signInDashboardUser(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
return signInDashboardUser(new AppIdentifier(null, null), storage,
main, email, password);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -160,7 +159,7 @@ public class Dashboard {
public static boolean deleteUserWithUserId(Main main, String userId)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return deleteUserWithUserId(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId);
return deleteUserWithUserId(new AppIdentifier(null, null), storage, userId);
}
public static boolean deleteUserWithUserId(AppIdentifier appIdentifier, Storage storage, String userId)
@ -202,7 +201,7 @@ public class Dashboard {
public static boolean deleteUserWithEmail(Main main, String email)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return deleteUserWithEmail(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, email);
return deleteUserWithEmail(new AppIdentifier(null, null), storage, email);
}
public static boolean deleteUserWithEmail(AppIdentifier appIdentifier, Storage storage, String email)
@ -224,7 +223,7 @@ public class Dashboard {
try {
Storage storage = StorageLayer.getStorage(main);
return updateUsersCredentialsWithUserId(
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, main, userId,
new AppIdentifier(null, null), storage, main, userId,
newEmail, newPassword);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -292,7 +291,7 @@ public class Dashboard {
public static DashboardUser getDashboardUserByEmail(Main main, String email)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getDashboardUserByEmail(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, email);
return getDashboardUserByEmail(new AppIdentifier(null, null), storage, email);
}
public static DashboardUser getDashboardUserByEmail(AppIdentifier appIdentifier, Storage storage, String email)
@ -306,7 +305,7 @@ public class Dashboard {
public static boolean revokeSessionWithSessionId(Main main, String sessionId)
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return revokeSessionWithSessionId(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, sessionId);
return revokeSessionWithSessionId(new AppIdentifier(null, null), storage, sessionId);
}
public static boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, Storage storage, String sessionId)
@ -321,7 +320,7 @@ public class Dashboard {
throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return getAllDashboardSessionsForUser(
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId);
new AppIdentifier(null, null), storage, userId);
}
public static DashboardSessionInfo[] getAllDashboardSessionsForUser(AppIdentifier appIdentifier, Storage storage,
@ -391,7 +390,7 @@ public class Dashboard {
public static boolean isValidUserSession(Main main, String sessionId)
throws StorageQueryException, UserSuspendedException {
Storage storage = StorageLayer.getStorage(main);
return isValidUserSession(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, main, sessionId);
return isValidUserSession(new AppIdentifier(null, null), storage, main, sessionId);
}
public static boolean isValidUserSession(AppIdentifier appIdentifier, Storage storage, Main main, String sessionId)

View File

@ -16,18 +16,7 @@
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;
@ -61,6 +50,14 @@ 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 {
@ -77,7 +74,7 @@ public class EmailPassword {
@TestOnly
public static long getPasswordResetTokenLifetimeForTests(Main main) {
try {
return getPasswordResetTokenLifetime(ResourceDistributor.getAppForTesting(), main);
return getPasswordResetTokenLifetime(new TenantIdentifier(null, null, null), main);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -93,7 +90,7 @@ public class EmailPassword {
throws DuplicateEmailException, StorageQueryException {
try {
Storage storage = StorageLayer.getStorage(main);
return signUp(ResourceDistributor.getAppForTesting(), storage,
return signUp(new TenantIdentifier(null, null, null), storage,
main, email, password);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -160,7 +157,7 @@ public class EmailPassword {
Storage storage = StorageLayer.getStorage(main);
return importUserWithPasswordHash(
ResourceDistributor.getAppForTesting(), storage, main, email,
new TenantIdentifier(null, null, null), storage, main, email,
passwordHash, hashingAlgorithm);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -218,7 +215,7 @@ public class EmailPassword {
public static ImportUserResponse createUserWithPasswordHash(TenantIdentifier tenantIdentifier, Storage storage,
@Nonnull String email,
@Nonnull String passwordHash, long timeJoined)
@Nonnull String passwordHash, @Nullable long timeJoined)
throws StorageQueryException, DuplicateEmailException, TenantOrAppNotFoundException,
StorageTransactionLogicException {
EmailPasswordSQLStorage epStorage = StorageUtils.getEmailPasswordStorage(storage);
@ -279,7 +276,7 @@ public class EmailPassword {
try {
Storage storage = StorageLayer.getStorage(main);
return importUserWithPasswordHash(
ResourceDistributor.getAppForTesting(), storage,
new TenantIdentifier(null, null, null), storage,
main, email, passwordHash, null);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -292,7 +289,7 @@ public class EmailPassword {
throws StorageQueryException, WrongCredentialsException {
try {
Storage storage = StorageLayer.getStorage(main);
return signIn(ResourceDistributor.getAppForTesting(), storage,
return signIn(new TenantIdentifier(null, null, null), storage,
main, email, password);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -356,7 +353,7 @@ public class EmailPassword {
try {
Storage storage = StorageLayer.getStorage(main);
return generatePasswordResetTokenBeforeCdi4_0(
ResourceDistributor.getAppForTesting(), storage,
new TenantIdentifier(null, null, null), storage,
main, userId);
} catch (TenantOrAppNotFoundException | BadPermissionException | WebserverAPI.BadRequestException e) {
throw new IllegalStateException(e);
@ -369,7 +366,7 @@ public class EmailPassword {
try {
Storage storage = StorageLayer.getStorage(main);
return generatePasswordResetToken(
ResourceDistributor.getAppForTesting(), storage,
new TenantIdentifier(null, null, null), storage,
main, userId, null);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -382,7 +379,7 @@ public class EmailPassword {
try {
Storage storage = StorageLayer.getStorage(main);
return generatePasswordResetToken(
ResourceDistributor.getAppForTesting(), storage,
new TenantIdentifier(null, null, null), storage,
main, userId, email);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
@ -459,7 +456,7 @@ public class EmailPassword {
StorageTransactionLogicException {
try {
Storage storage = StorageLayer.getStorage(main);
return resetPassword(ResourceDistributor.getAppForTesting(), storage,
return resetPassword(new TenantIdentifier(null, null, null), storage,
main, token, password);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -533,7 +530,7 @@ public class EmailPassword {
StorageTransactionLogicException {
try {
Storage storage = StorageLayer.getStorage(main);
return consumeResetPasswordToken(ResourceDistributor.getAppForTesting(), storage,
return consumeResetPasswordToken(new TenantIdentifier(null, null, null), storage,
token);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -631,7 +628,7 @@ public class EmailPassword {
UnknownUserIdException, DuplicateEmailException, EmailChangeNotAllowedException {
try {
Storage storage = StorageLayer.getStorage(main);
updateUsersEmailOrPassword(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
updateUsersEmailOrPassword(new AppIdentifier(null, null), storage,
main, userId, email, password);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -728,7 +725,7 @@ public class EmailPassword {
throws StorageQueryException {
try {
Storage storage = StorageLayer.getStorage(main);
return getUserUsingId(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId);
return getUserUsingId(new AppIdentifier(null, null), storage, userId);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}

View File

@ -30,8 +30,6 @@ 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;
@ -44,9 +42,6 @@ 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());
@ -80,7 +75,7 @@ public class PasswordHashing extends ResourceDistributor.SingletonResource {
@TestOnly
public String createHashWithSalt(String password) {
try {
return createHashWithSalt(ResourceDistributor.getAppForTesting().toAppIdentifier(), password);
return createHashWithSalt(new AppIdentifier(null, null), password);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -89,10 +84,6 @@ 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();
@ -117,10 +108,6 @@ 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.commons.codec.binary.Base64;
import org.apache.tomcat.util.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);
byte[] decodedSaltBytes = Base64.decodeBase64(byteArrTemp, 0, byteArrTemp.length);
byteArrTemp = response.saltSeparator.getBytes(StandardCharsets.US_ASCII);
byte[] decodedSaltSepBytes = Base64.decodeBase64(byteArrTemp);
byte[] decodedSaltSepBytes = Base64.decodeBase64(byteArrTemp, 0, byteArrTemp.length);
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);
byte[] signerBytes = Base64.decodeBase64(byteArrTemp, 0, byteArrTemp.length);
try {
String CIPHER = "AES/CTR/NoPadding";

View File

@ -17,7 +17,6 @@
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;
@ -45,7 +44,7 @@ public class EmailVerification {
public static long getEmailVerificationTokenLifetimeForTests(Main main) {
try {
return getEmailVerificationTokenLifetime(
ResourceDistributor.getAppForTesting(), main);
new TenantIdentifier(null, null, null), main);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -63,7 +62,7 @@ public class EmailVerification {
try {
Storage storage = StorageLayer.getStorage(main);
return generateEmailVerificationToken(
ResourceDistributor.getAppForTesting(), storage,
new TenantIdentifier(null, null, null), storage,
main, userId, email);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
@ -108,7 +107,7 @@ public class EmailVerification {
EmailVerificationInvalidTokenException, NoSuchAlgorithmException, StorageTransactionLogicException {
try {
Storage storage = StorageLayer.getStorage(main);
return verifyEmail(ResourceDistributor.getAppForTesting(), storage, token);
return verifyEmail(new TenantIdentifier(null, null, null), storage, token);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -183,7 +182,7 @@ public class EmailVerification {
public static boolean isEmailVerified(Main main, String userId,
String email) throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
return isEmailVerified(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
return isEmailVerified(new AppIdentifier(null, null), storage,
userId, email);
}
@ -197,7 +196,7 @@ public class EmailVerification {
public static void revokeAllTokens(Main main, String userId,
String email) throws StorageQueryException {
Storage storage = StorageLayer.getStorage(main);
revokeAllTokens(ResourceDistributor.getAppForTesting(), storage,
revokeAllTokens(new TenantIdentifier(null, null, null), storage,
userId, email);
}
@ -212,7 +211,7 @@ public class EmailVerification {
String email) throws StorageQueryException {
try {
Storage storage = StorageLayer.getStorage(main);
unverifyEmail(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId, email);
unverifyEmail(new AppIdentifier(null, null), storage, userId, email);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -250,7 +249,7 @@ public class EmailVerification {
try {
StorageUtils.getEmailVerificationStorage(StorageLayer.getStorage(main))
.addEmailVerificationToken(ResourceDistributor.getAppForTesting(),
.addEmailVerificationToken(new TenantIdentifier(null, null, null),
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"), SAML("saml");
DASHBOARD_LOGIN("dashboard_login"), MFA("mfa"), SECURITY("security"), OAUTH("oauth");
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(ResourceDistributor.getAppForTesting(), RESOURCE_KEY);
.getResource(new AppIdentifier(null, null), RESOURCE_KEY);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,458 +0,0 @@
/*
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.supertokens.inmemorydb.queries;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
import io.supertokens.inmemorydb.Start;
import io.supertokens.inmemorydb.config.Config;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.saml.SAMLClaimsInfo;
import io.supertokens.pluginInterface.saml.SAMLClient;
import io.supertokens.pluginInterface.saml.SAMLRelayStateInfo;
public class SAMLQueries {
public static String getQueryToCreateSAMLClientsTable(Start start) {
String table = Config.getConfig(start).getSAMLClientsTable();
String tenantsTable = Config.getConfig(start).getTenantsTable();
// @formatter:off
return "CREATE TABLE IF NOT EXISTS " + table + " ("
+ "app_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "client_id VARCHAR(255) NOT NULL,"
+ "client_secret TEXT,"
+ "sso_login_url TEXT NOT NULL,"
+ "redirect_uris TEXT NOT NULL," // store JsonArray.toString()
+ "default_redirect_uri VARCHAR(1024) NOT NULL,"
+ "idp_entity_id VARCHAR(1024),"
+ "idp_signing_certificate TEXT,"
+ "allow_idp_initiated_login BOOLEAN NOT NULL DEFAULT FALSE,"
+ "enable_request_signing BOOLEAN NOT NULL DEFAULT TRUE,"
+ "created_at BIGINT NOT NULL,"
+ "updated_at BIGINT NOT NULL,"
+ "UNIQUE (app_id, tenant_id, idp_entity_id),"
+ "PRIMARY KEY (app_id, tenant_id, client_id),"
+ "FOREIGN KEY (app_id, tenant_id) REFERENCES " + tenantsTable + " (app_id, tenant_id) ON DELETE CASCADE"
+ ");";
// @formatter:on
}
public static String getQueryToCreateSAMLClientsAppIdTenantIdIndex(Start start) {
String table = Config.getConfig(start).getSAMLClientsTable();
return "CREATE INDEX IF NOT EXISTS saml_clients_app_tenant_index ON " + table + "(app_id, tenant_id);";
}
public static String getQueryToCreateSAMLRelayStateTable(Start start) {
String table = Config.getConfig(start).getSAMLRelayStateTable();
String tenantsTable = Config.getConfig(start).getTenantsTable();
// @formatter:off
return "CREATE TABLE IF NOT EXISTS " + table + " ("
+ "app_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "relay_state VARCHAR(255) NOT NULL,"
+ "client_id VARCHAR(255) NOT NULL,"
+ "state TEXT,"
+ "redirect_uri VARCHAR(1024) NOT NULL,"
+ "created_at BIGINT NOT NULL,"
+ "expires_at BIGINT NOT NULL,"
+ "PRIMARY KEY (relay_state)," // relayState must be unique
+ "FOREIGN KEY (app_id, tenant_id) REFERENCES " + tenantsTable + " (app_id, tenant_id) ON DELETE CASCADE"
+ ");";
// @formatter:on
}
public static String getQueryToCreateSAMLRelayStateAppIdTenantIdIndex(Start start) {
String table = Config.getConfig(start).getSAMLRelayStateTable();
return "CREATE INDEX IF NOT EXISTS saml_relay_state_app_tenant_index ON " + table + "(app_id, tenant_id);";
}
public static String getQueryToCreateSAMLRelayStateExpiresAtIndex(Start start) {
String table = Config.getConfig(start).getSAMLRelayStateTable();
return "CREATE INDEX IF NOT EXISTS saml_relay_state_expires_at_index ON " + table + "(expires_at);";
}
public static String getQueryToCreateSAMLClaimsTable(Start start) {
String table = Config.getConfig(start).getSAMLClaimsTable();
String tenantsTable = Config.getConfig(start).getTenantsTable();
// @formatter:off
return "CREATE TABLE IF NOT EXISTS " + table + " ("
+ "app_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',"
+ "client_id VARCHAR(255) NOT NULL,"
+ "code VARCHAR(255) NOT NULL,"
+ "claims TEXT NOT NULL,"
+ "created_at BIGINT NOT NULL,"
+ "expires_at BIGINT NOT NULL,"
+ "PRIMARY KEY (code),"
+ "FOREIGN KEY (app_id, tenant_id) REFERENCES " + tenantsTable + " (app_id, tenant_id) ON DELETE CASCADE"
+ ");";
// @formatter:on
}
public static String getQueryToCreateSAMLClaimsAppIdTenantIdIndex(Start start) {
String table = Config.getConfig(start).getSAMLClaimsTable();
return "CREATE INDEX IF NOT EXISTS saml_claims_app_tenant_index ON " + table + "(app_id, tenant_id);";
}
public static String getQueryToCreateSAMLClaimsExpiresAtIndex(Start start) {
String table = Config.getConfig(start).getSAMLClaimsTable();
return "CREATE INDEX IF NOT EXISTS saml_claims_expires_at_index ON " + table + "(expires_at);";
}
public static void saveRelayStateInfo(Start start, TenantIdentifier tenantIdentifier,
String relayState, String clientId, String state, String redirectURI, long relayStateValidity)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLRelayStateTable();
String QUERY = "INSERT INTO " + table +
" (app_id, tenant_id, relay_state, client_id, state, redirect_uri, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
try {
update(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, relayState);
pst.setString(4, clientId);
if (state != null) {
pst.setString(5, state);
} else {
pst.setNull(5, java.sql.Types.VARCHAR);
}
pst.setString(6, redirectURI);
pst.setLong(7, System.currentTimeMillis());
pst.setLong(8, System.currentTimeMillis() + relayStateValidity);
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static SAMLRelayStateInfo getRelayStateInfo(Start start, TenantIdentifier tenantIdentifier, String relayState)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLRelayStateTable();
String QUERY = "SELECT client_id, state, redirect_uri, expires_at FROM " + table
+ " WHERE app_id = ? AND tenant_id = ? AND relay_state = ? AND expires_at >= ?";
try {
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, relayState);
pst.setLong(4, System.currentTimeMillis());
}, result -> {
if (result.next()) {
String clientId = result.getString("client_id");
String state = result.getString("state"); // may be null
String redirectURI = result.getString("redirect_uri");
return new SAMLRelayStateInfo(relayState, clientId, state, redirectURI);
}
return null;
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static void saveSAMLClaims(Start start, TenantIdentifier tenantIdentifier, String clientId, String code, String claimsJson, long claimsValidity)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClaimsTable();
String QUERY = "INSERT INTO " + table +
" (app_id, tenant_id, client_id, code, claims, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)";
try {
update(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, clientId);
pst.setString(4, code);
pst.setString(5, claimsJson);
pst.setLong(6, System.currentTimeMillis());
pst.setLong(7, System.currentTimeMillis() + claimsValidity);
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static SAMLClaimsInfo getSAMLClaimsAndRemoveCode(Start start, TenantIdentifier tenantIdentifier, String code)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClaimsTable();
String QUERY = "SELECT client_id, claims FROM " + table + " WHERE app_id = ? AND tenant_id = ? AND code = ? AND expires_at >= ?";
try {
SAMLClaimsInfo claimsInfo = execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, code);
pst.setLong(4, System.currentTimeMillis());
}, result -> {
if (result.next()) {
String clientId = result.getString("client_id");
JsonObject claims = com.google.gson.JsonParser.parseString(result.getString("claims")).getAsJsonObject();
return new SAMLClaimsInfo(clientId, claims);
}
return null;
});
if (claimsInfo != null) {
String DELETE = "DELETE FROM " + table + " WHERE app_id = ? AND tenant_id = ? AND code = ?";
update(start, DELETE, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, code);
});
}
return claimsInfo;
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static SAMLClient createOrUpdateSAMLClient(
Start start,
TenantIdentifier tenantIdentifier,
String clientId,
String clientSecret,
String ssoLoginURL,
String redirectURIsJson,
String defaultRedirectURI,
String idpEntityId,
String idpSigningCertificate,
boolean allowIDPInitiatedLogin,
boolean enableRequestSigning)
throws StorageQueryException, SQLException {
String table = Config.getConfig(start).getSAMLClientsTable();
String QUERY = "INSERT INTO " + table +
" (app_id, tenant_id, client_id, client_secret, sso_login_url, redirect_uris, default_redirect_uri, idp_entity_id, idp_signing_certificate, allow_idp_initiated_login, enable_request_signing, created_at, updated_at) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " +
"ON CONFLICT (app_id, tenant_id, client_id) DO UPDATE SET " +
"client_secret = ?, sso_login_url = ?, redirect_uris = ?, default_redirect_uri = ?, idp_entity_id = ?, idp_signing_certificate = ?, allow_idp_initiated_login = ?, enable_request_signing = ?, updated_at = ?";
long now = System.currentTimeMillis();
update(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, clientId);
if (clientSecret != null) {
pst.setString(4, clientSecret);
} else {
pst.setNull(4, Types.VARCHAR);
}
pst.setString(5, ssoLoginURL);
pst.setString(6, redirectURIsJson);
pst.setString(7, defaultRedirectURI);
if (idpEntityId != null) {
pst.setString(8, idpEntityId);
} else {
pst.setNull(8, java.sql.Types.VARCHAR);
}
if (idpSigningCertificate != null) {
pst.setString(9, idpSigningCertificate);
} else {
pst.setNull(9, Types.VARCHAR);
}
pst.setBoolean(10, allowIDPInitiatedLogin);
pst.setBoolean(11, enableRequestSigning);
pst.setLong(12, now);
pst.setLong(13, now);
if (clientSecret != null) {
pst.setString(14, clientSecret);
} else {
pst.setNull(14, Types.VARCHAR);
}
pst.setString(15, ssoLoginURL);
pst.setString(16, redirectURIsJson);
pst.setString(17, defaultRedirectURI);
if (idpEntityId != null) {
pst.setString(18, idpEntityId);
} else {
pst.setNull(18, java.sql.Types.VARCHAR);
}
if (idpSigningCertificate != null) {
pst.setString(19, idpSigningCertificate);
} else {
pst.setNull(19, Types.VARCHAR);
}
pst.setBoolean(20, allowIDPInitiatedLogin);
pst.setBoolean(21, enableRequestSigning);
pst.setLong(22, now);
});
return getSAMLClient(start, tenantIdentifier, clientId);
}
public static SAMLClient getSAMLClient(Start start, TenantIdentifier tenantIdentifier, String clientId)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClientsTable();
String QUERY = "SELECT client_id, client_secret, sso_login_url, redirect_uris, default_redirect_uri, idp_entity_id, idp_signing_certificate, allow_idp_initiated_login, enable_request_signing FROM " + table
+ " WHERE app_id = ? AND tenant_id = ? AND client_id = ?";
try {
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, clientId);
}, result -> {
if (result.next()) {
String fetchedClientId = result.getString("client_id");
String clientSecret = result.getString("client_secret");
String ssoLoginURL = result.getString("sso_login_url");
String redirectUrisJson = result.getString("redirect_uris");
String defaultRedirectURI = result.getString("default_redirect_uri");
String idpEntityId = result.getString("idp_entity_id");
String idpSigningCertificate = result.getString("idp_signing_certificate");
boolean allowIDPInitiatedLogin = result.getBoolean("allow_idp_initiated_login");
boolean enableRequestSigning = result.getBoolean("enable_request_signing");
JsonArray redirectURIs = JsonParser.parseString(redirectUrisJson).getAsJsonArray();
return new SAMLClient(fetchedClientId, clientSecret, ssoLoginURL, redirectURIs, defaultRedirectURI, idpEntityId, idpSigningCertificate, allowIDPInitiatedLogin, enableRequestSigning);
}
return null;
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static SAMLClient getSAMLClientByIDPEntityId(Start start, TenantIdentifier tenantIdentifier, String idpEntityId) throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClientsTable();
String QUERY = "SELECT client_id, client_secret, sso_login_url, redirect_uris, default_redirect_uri, idp_entity_id, idp_signing_certificate, allow_idp_initiated_login, enable_request_signing FROM " + table
+ " WHERE app_id = ? AND tenant_id = ? AND idp_entity_id = ?";
try {
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, idpEntityId);
}, result -> {
if (result.next()) {
String fetchedClientId = result.getString("client_id");
String clientSecret = result.getString("client_secret");
String ssoLoginURL = result.getString("sso_login_url");
String redirectUrisJson = result.getString("redirect_uris");
String defaultRedirectURI = result.getString("default_redirect_uri");
String fetchedIdpEntityId = result.getString("idp_entity_id");
String idpSigningCertificate = result.getString("idp_signing_certificate");
boolean allowIDPInitiatedLogin = result.getBoolean("allow_idp_initiated_login");
boolean enableRequestSigning = result.getBoolean("enable_request_signing");
JsonArray redirectURIs = JsonParser.parseString(redirectUrisJson).getAsJsonArray();
return new SAMLClient(fetchedClientId, clientSecret, ssoLoginURL, redirectURIs, defaultRedirectURI, fetchedIdpEntityId, idpSigningCertificate, allowIDPInitiatedLogin, enableRequestSigning);
}
return null;
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static List<SAMLClient> getSAMLClients(Start start, TenantIdentifier tenantIdentifier)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClientsTable();
String QUERY = "SELECT client_id, client_secret, sso_login_url, redirect_uris, default_redirect_uri, idp_entity_id, idp_signing_certificate, allow_idp_initiated_login, enable_request_signing FROM " + table
+ " WHERE app_id = ? AND tenant_id = ?";
try {
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
}, result -> {
List<SAMLClient> clients = new ArrayList<>();
while (result.next()) {
String fetchedClientId = result.getString("client_id");
String clientSecret = result.getString("client_secret");
String ssoLoginURL = result.getString("sso_login_url");
String redirectUrisJson = result.getString("redirect_uris");
String defaultRedirectURI = result.getString("default_redirect_uri");
String idpEntityId = result.getString("idp_entity_id");
String idpSigningCertificate = result.getString("idp_signing_certificate");
boolean allowIDPInitiatedLogin = result.getBoolean("allow_idp_initiated_login");
boolean enableRequestSigning = result.getBoolean("enable_request_signing");
JsonArray redirectURIs = JsonParser.parseString(redirectUrisJson).getAsJsonArray();
clients.add(new SAMLClient(fetchedClientId, clientSecret, ssoLoginURL, redirectURIs, defaultRedirectURI, idpEntityId, idpSigningCertificate, allowIDPInitiatedLogin, enableRequestSigning));
}
return clients;
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static boolean removeSAMLClient(Start start, TenantIdentifier tenantIdentifier, String clientId)
throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClientsTable();
String QUERY = "DELETE FROM " + table + " WHERE app_id = ? AND tenant_id = ? AND client_id = ?";
try {
return update(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
pst.setString(3, clientId);
}) > 0;
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static void removeExpiredSAMLCodesAndRelayStates(Start start) throws StorageQueryException {
try {
{
String QUERY = "DELETE FROM " + Config.getConfig(start).getSAMLClaimsTable() + " WHERE expires_at <= ?";
update(start, QUERY, pst -> {
pst.setLong(1, System.currentTimeMillis());
});
}
{
String QUERY = "DELETE FROM " + Config.getConfig(start).getSAMLRelayStateTable() + " WHERE expires_at <= ?";
update(start, QUERY, pst -> {
pst.setLong(1, System.currentTimeMillis());
});
}
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
public static int countSAMLClients(Start start, TenantIdentifier tenantIdentifier) throws StorageQueryException {
String table = Config.getConfig(start).getSAMLClientsTable();
String QUERY = "SELECT COUNT(*) as c FROM " + table
+ " WHERE app_id = ? AND tenant_id = ?";
try {
return execute(start, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
pst.setString(2, tenantIdentifier.getTenantId());
}, result -> {
if (result.next()) {
return result.getInt("c");
}
return 0;
});
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
}

View File

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

View File

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

View File

@ -23,7 +23,6 @@ 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;
@ -55,7 +54,7 @@ public class JWTSigningFunctions {
throws StorageQueryException, StorageTransactionLogicException, NoSuchAlgorithmException,
InvalidKeySpecException, JWTCreationException, UnsupportedJWTSigningAlgorithmException {
try {
return createJWTToken(ResourceDistributor.getAppForTesting().toAppIdentifier(), main, algorithm, payload, jwksDomain,
return createJWTToken(new AppIdentifier(null, null), main, algorithm, payload, jwksDomain,
jwtValidityInSeconds, useDynamicKey);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);

View File

@ -33,8 +33,6 @@ 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;
@ -118,7 +116,6 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
return StorageLayer.getMultitenancyStorage(main).getAllTenants();
}
@WithinOtelSpan
public List<TenantIdentifier> refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(
boolean reloadAllResources) {
try {
@ -236,7 +233,6 @@ 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 = 15000;
private static final int READ_TIMEOUT = 15000;
private static final int CONNECTION_TIMEOUT = 5000;
private static final int READ_TIMEOUT = 5000;
public static Response doGet(String url, Map<String, String> headers, Map<String, String> queryParams) throws IOException {
if (queryParams == null) {

View File

@ -16,7 +16,6 @@
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;
@ -56,12 +55,6 @@ 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),
@ -73,7 +66,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), TelemetryProvider.getInstance(main));
Config.getBaseConfig(main).getErrorLogPath(main));
}
try {
// we wait here for a bit so that the loggers can be properly initialised..
@ -119,9 +112,9 @@ public class Logging extends ResourceDistributor.SingletonResource {
try {
msg = msg.trim();
if (getInstance(main) != null) {
String formattedMsg = getFormattedMessage(tenantIdentifier, msg);
getInstance(main).infoLogger.debug(formattedMsg);
TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, formattedMsg, "debug");
getInstance(main).infoLogger.debug(getFormattedMessage(tenantIdentifier, msg));
TelemetryProvider.createLogEvent(main, tenantIdentifier, getFormattedMessage(tenantIdentifier, msg),
"debug");
}
} catch (NullPointerException e) {
// sometimes logger.debug throws a null pointer exception...
@ -173,7 +166,7 @@ public class Logging extends ResourceDistributor.SingletonResource {
getInstance(main).infoLogger.info(msg);
}
TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, msg, "info");
TelemetryProvider.createLogEvent(main, tenantIdentifier, msg, "info");
} catch (NullPointerException ignored) {
}
}
@ -187,7 +180,8 @@ 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");
TelemetryProvider.createLogEvent(main, tenantIdentifier, msg, "warn");
}
} catch (NullPointerException ignored) {
}
@ -207,9 +201,10 @@ public class Logging extends ResourceDistributor.SingletonResource {
try {
err = err.trim();
if (getInstance(main) != null) {
String formattedMessage = getFormattedMessage(tenantIdentifier, err);
getInstance(main).errorLogger.error(formattedMessage);
TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, formattedMessage, "error");
getInstance(main).errorLogger.error(getFormattedMessage(tenantIdentifier, err));
TelemetryProvider.createLogEvent(main, tenantIdentifier, getFormattedMessage(tenantIdentifier, err),
"error");
}
if (toConsoleAsWell || getInstance(main) == null) {
systemErr(prependTenantIdentifierToMessage(tenantIdentifier, err));
@ -243,8 +238,8 @@ 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),
TelemetryProvider
.createLogEvent(main, tenantIdentifier, getFormattedMessage(tenantIdentifier, message, e),
"error");
}
if (toConsoleAsWell || getInstance(main) == null) {

View File

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

View File

@ -1,690 +0,0 @@
/*
* 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

@ -1,50 +0,0 @@
/*
* 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

@ -1,315 +0,0 @@
/*
* 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

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

View File

@ -1,20 +0,0 @@
/*
* 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

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

View File

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

View File

@ -1,20 +0,0 @@
/*
* 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

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

View File

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

View File

@ -21,7 +21,6 @@ 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;
@ -176,7 +175,7 @@ public class AccessToken {
throws StorageQueryException, StorageTransactionLogicException, TryRefreshTokenException,
UnsupportedJWTSigningAlgorithmException {
try {
return getInfoFromAccessToken(ResourceDistributor.getAppForTesting().toAppIdentifier(), main, token, doAntiCsrfCheck);
return getInfoFromAccessToken(new AppIdentifier(null, null), main, token, doAntiCsrfCheck);
} catch (TenantOrAppNotFoundException e) {
throw new IllegalStateException(e);
}
@ -187,7 +186,7 @@ public class AccessToken {
public static AccessTokenInfo getInfoFromAccessTokenWithoutVerifying(@Nonnull String token)
throws JWTException, TryRefreshTokenException {
return getInfoFromAccessTokenWithoutVerifying(
ResourceDistributor.getAppForTesting().toAppIdentifier(), token);
new AppIdentifier(null, null), token);
}
public static AccessTokenInfo getInfoFromAccessTokenWithoutVerifying(AppIdentifier appIdentifier,
@ -210,7 +209,7 @@ public class AccessToken {
NoSuchAlgorithmException, InvalidKeySpecException, SignatureException,
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError {
try {
return createNewAccessToken(ResourceDistributor.getAppForTesting(), main, sessionHandle, userId, userId,
return createNewAccessToken(new TenantIdentifier(null, null, null), main, sessionHandle, userId, userId,
refreshTokenHash1,
parentRefreshTokenHash1,
userData, antiCsrfToken, expiryTime, version, useStaticKey);
@ -276,7 +275,7 @@ public class AccessToken {
NoSuchAlgorithmException, InvalidKeySpecException, SignatureException,
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError {
try {
return createNewAccessTokenV1(ResourceDistributor.getAppForTesting(), main, sessionHandle, userId,
return createNewAccessTokenV1(new TenantIdentifier(null, null, null), main, sessionHandle, userId,
refreshTokenHash1,
parentRefreshTokenHash1, userData, antiCsrfToken);
} catch (TenantOrAppNotFoundException e) {

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