Compare commits
142 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
c9e8287b30 | |
|
|
5f2e57ba31 | |
|
|
bce323172f | |
|
|
1312069b0d | |
|
|
1b1b2b2bdf | |
|
|
e2ce5828f0 | |
|
|
8ee1665c72 | |
|
|
d524bdb9d9 | |
|
|
be57314b35 | |
|
|
34eeda50fe | |
|
|
058f6f69df | |
|
|
e671bcb492 | |
|
|
c4e8afbcd1 | |
|
|
384d688b85 | |
|
|
97dfa59faf | |
|
|
9d3253f71e | |
|
|
9b84d8cdef | |
|
|
879292770a | |
|
|
58956c3675 | |
|
|
d924ed3e44 | |
|
|
ceef7921f2 | |
|
|
7444015f3d | |
|
|
f82174d398 | |
|
|
15933a3216 | |
|
|
c1e3ba7516 | |
|
|
018877e803 | |
|
|
7a75827d61 | |
|
|
5fbe206e50 | |
|
|
a07e41591a | |
|
|
f09f011e21 | |
|
|
f67feb4884 | |
|
|
7c0d335967 | |
|
|
497c1f677a | |
|
|
5970840f9e | |
|
|
b9e05884a4 | |
|
|
378498b979 | |
|
|
2f31ec4a1c | |
|
|
1cedc88a94 | |
|
|
6b3b882b3f | |
|
|
923e70eca4 | |
|
|
64b421a130 | |
|
|
b1b98c35f0 | |
|
|
00dbb22236 | |
|
|
2496a96d42 | |
|
|
e76fbbc31c | |
|
|
a90148d0eb | |
|
|
68711923e6 | |
|
|
faaabe8db2 | |
|
|
d2c2bd4a45 | |
|
|
c68631d6af | |
|
|
b71264b18e | |
|
|
495a3f25cb | |
|
|
a877255eaf | |
|
|
f09908dc14 | |
|
|
29782b00a2 | |
|
|
a135b46346 | |
|
|
d46136027f | |
|
|
91fcafe339 | |
|
|
12e2f04c4c | |
|
|
ebeb635cfb | |
|
|
e665994285 | |
|
|
72961d814f | |
|
|
e0e39af30b | |
|
|
f1cc209f8f | |
|
|
be24450387 | |
|
|
650006295e | |
|
|
999b1021ad | |
|
|
6bd62b884b | |
|
|
a964e003aa | |
|
|
6b02c7becd | |
|
|
9430e2fcb4 | |
|
|
e13612adec | |
|
|
c6122ad2fd | |
|
|
3e2c1c021d | |
|
|
36a280bff3 | |
|
|
bd691696a3 | |
|
|
5d0c345246 | |
|
|
3d52c9d1b8 | |
|
|
c12901cd84 | |
|
|
d2131cf974 | |
|
|
6e5a36cc1e | |
|
|
c6acdbd814 | |
|
|
c18965126f | |
|
|
dd8aa42c0d | |
|
|
affa77169c | |
|
|
33f28e1bc6 | |
|
|
96def240b3 | |
|
|
f45952d99f | |
|
|
bd5a279b11 | |
|
|
1ef697f925 | |
|
|
205fe3dd4e | |
|
|
482b0dbfcb | |
|
|
abf8a2b65a | |
|
|
37de0a8e3a | |
|
|
f0c7e5f8fd | |
|
|
06c176d243 | |
|
|
2607c84352 | |
|
|
5abba306f9 | |
|
|
ab7375aa92 | |
|
|
7e93048a35 | |
|
|
10768f7ee9 | |
|
|
0171fcd388 | |
|
|
aeef6e3406 | |
|
|
611b5bc2e1 | |
|
|
0c03d9fd70 | |
|
|
25fccc6849 | |
|
|
a949c86750 | |
|
|
1f7e58d2ba | |
|
|
1efe87fe53 | |
|
|
122b574941 | |
|
|
9561424777 | |
|
|
89f38fd7ff | |
|
|
5bfa99251a | |
|
|
3755ed1f3c | |
|
|
edaaf19e72 | |
|
|
85118ac3ee | |
|
|
f2be9beb56 | |
|
|
16b17beeaa | |
|
|
9e24d0ea00 | |
|
|
3c879c4273 | |
|
|
ff95845f75 | |
|
|
27aae9f205 | |
|
|
59f8673979 | |
|
|
c9b120d35a | |
|
|
c394e1bac8 | |
|
|
54d97dd569 | |
|
|
61a67f5479 | |
|
|
ea63eafdf0 | |
|
|
5693b4b0aa | |
|
|
e91afcad8e | |
|
|
f69477a520 | |
|
|
2d31f3a254 | |
|
|
6de2cbb3c1 | |
|
|
d14426a203 | |
|
|
2baba1a43e | |
|
|
aaa3992e4e | |
|
|
cc75411bf7 | |
|
|
9d871a6260 | |
|
|
ab109d3370 | |
|
|
177ee2f49a | |
|
|
36f60799ed | |
|
|
4db84200fa |
|
|
@ -1,8 +1,9 @@
|
|||
FROM ubuntu:16.04
|
||||
FROM ubuntu:22.04
|
||||
|
||||
RUN apt-get update && apt-get upgrade -y
|
||||
RUN apt-get update -y
|
||||
#&& apt-get upgrade -y
|
||||
|
||||
RUN apt-get install build-essential -y
|
||||
RUN apt-get install build-essential -y --fix-missing
|
||||
|
||||
RUN echo "mysql-server mysql-server/root_password password root" | debconf-set-selections
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ RUN apt install mysql-server -y
|
|||
|
||||
RUN usermod -d /var/lib/mysql/ mysql
|
||||
|
||||
RUN mkdir /var/run/mysqld
|
||||
RUN [ -d /var/run/mysqld ] || mkdir -p /var/run/mysqld
|
||||
|
||||
ADD ./runMySQL.sh /runMySQL.sh
|
||||
|
||||
|
|
@ -22,36 +23,42 @@ RUN apt-get install -y git-core
|
|||
|
||||
RUN apt-get install -y wget
|
||||
|
||||
# Install OpenJDK 12
|
||||
RUN wget https://download.java.net/java/GA/jdk12.0.2/e482c34c86bd4bf8b56c0b35558996b9/10/GPL/openjdk-12.0.2_linux-x64_bin.tar.gz
|
||||
|
||||
RUN mkdir /usr/java
|
||||
|
||||
RUN mv openjdk-12.0.2_linux-x64_bin.tar.gz /usr/java
|
||||
|
||||
RUN cd /usr/java && tar -xzvf openjdk-12.0.2_linux-x64_bin.tar.gz
|
||||
|
||||
RUN echo 'JAVA_HOME=/usr/java/jdk-12.0.2' >> /etc/profile
|
||||
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
|
||||
|
||||
RUN apt-get install jq -y
|
||||
|
||||
RUN apt-get install curl -y
|
||||
|
||||
RUN apt-get install unzip -y
|
||||
|
||||
# Install OpenJDK 15.0.1
|
||||
RUN wget https://download.java.net/java/GA/jdk15.0.1/51f4f36ad4ef43e39d0dfdbaf6549e32/9/GPL/openjdk-15.0.1_linux-x64_bin.tar.gz
|
||||
# Install OpenJDK 21.0.7
|
||||
RUN wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz
|
||||
|
||||
RUN mv openjdk-15.0.1_linux-x64_bin.tar.gz /usr/java
|
||||
|
||||
RUN cd /usr/java && tar -xzvf openjdk-15.0.1_linux-x64_bin.tar.gz
|
||||
|
||||
RUN echo 'JAVA_HOME=/usr/java/jdk-15.0.1' >> /etc/profile
|
||||
RUN mv OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz /usr/java
|
||||
RUN cd /usr/java && tar -xzvf OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz -C /usr/java/
|
||||
RUN mv /usr/java/jdk-21.0.7+6 /usr/java/jdk-21.0.7
|
||||
RUN echo 'JAVA_HOME=/usr/java/jdk-21.0.7' >> /etc/profile
|
||||
RUN echo 'JRE_HOME=/usr/java/jdk-21.0.7' >> /etc/profile
|
||||
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
|
||||
RUN echo 'export JAVA_HOME' >> /etc/profile
|
||||
RUN echo 'export JRE_HOME' >> /etc/profile
|
||||
RUN echo 'export PATH' >> /etc/profile
|
||||
|
||||
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-12.0.2/bin/java" 1
|
||||
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-12.0.2/bin/javac" 1
|
||||
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-21.0.7/bin/java" 1
|
||||
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-21.0.7/bin/javac" 1
|
||||
|
||||
#install postgres 13
|
||||
# Import Repository Signing Key
|
||||
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
|
||||
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata
|
||||
RUN apt install curl gpg gnupg2 software-properties-common apt-transport-https lsb-release ca-certificates sudo -y
|
||||
# Add PostgreSQL repository
|
||||
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
|
||||
# Update again
|
||||
RUN apt update
|
||||
# Install PostgreSQL 13
|
||||
RUN apt install -y postgresql-13
|
||||
# Verify PostgreSQL 13 Installation on Ubuntu 22.04|20.04|18.04
|
||||
RUN psql --version
|
||||
# Manage PostgreSQL 13 service
|
||||
#you can manage with `service postgresql start`
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
FROM ubuntu:16.04
|
||||
|
||||
RUN apt-get update && apt-get upgrade -y
|
||||
|
||||
RUN apt-get install build-essential -y
|
||||
|
||||
RUN echo "mysql-server mysql-server/root_password password root" | debconf-set-selections
|
||||
|
||||
RUN echo "mysql-server mysql-server/root_password_again password root" | debconf-set-selections
|
||||
|
||||
RUN apt install mysql-server -y
|
||||
|
||||
RUN usermod -d /var/lib/mysql/ mysql
|
||||
|
||||
RUN mkdir /var/run/mysqld
|
||||
|
||||
ADD ./runMySQL.sh /runMySQL.sh
|
||||
|
||||
RUN chmod +x /runMySQL.sh
|
||||
|
||||
RUN apt-get install -y git-core
|
||||
|
||||
RUN apt-get install -y wget
|
||||
|
||||
# Install OpenJDK 12
|
||||
RUN wget https://download.java.net/java/GA/jdk12.0.2/e482c34c86bd4bf8b56c0b35558996b9/10/GPL/openjdk-12.0.2_linux-x64_bin.tar.gz
|
||||
|
||||
RUN mkdir /usr/java
|
||||
|
||||
RUN mv openjdk-12.0.2_linux-x64_bin.tar.gz /usr/java
|
||||
|
||||
RUN cd /usr/java && tar -xzvf openjdk-12.0.2_linux-x64_bin.tar.gz
|
||||
|
||||
RUN echo 'JAVA_HOME=/usr/java/jdk-12.0.2' >> /etc/profile
|
||||
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
|
||||
|
||||
RUN apt-get install jq -y
|
||||
|
||||
RUN apt-get install curl -y
|
||||
|
||||
RUN apt-get install unzip -y
|
||||
|
||||
# Install OpenJDK 21.0.7
|
||||
RUN wget https://download.java.net/java/GA/jdk21.0.7/51f4f36ad4ef43e39d0dfdbaf6549e32/9/GPL/openjdk-21.0.7_linux-x64_bin.tar.gz
|
||||
|
||||
RUN mv openjdk-21.0.7_linux-x64_bin.tar.gz /usr/java
|
||||
|
||||
RUN cd /usr/java && tar -xzvf openjdk-21.0.7_linux-x64_bin.tar.gz
|
||||
|
||||
RUN echo 'JAVA_HOME=/usr/java/jdk-21.0.7' >> /etc/profile
|
||||
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
|
||||
RUN echo 'export JAVA_HOME' >> /etc/profile
|
||||
RUN echo 'export JRE_HOME' >> /etc/profile
|
||||
RUN echo 'export PATH' >> /etc/profile
|
||||
|
||||
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-12.0.2/bin/java" 1
|
||||
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-12.0.2/bin/javac" 1
|
||||
|
|
@ -22,39 +22,29 @@ RUN apt-get install -y git-core
|
|||
|
||||
RUN apt-get install -y wget
|
||||
|
||||
# Install OpenJDK 12
|
||||
RUN wget https://download.java.net/java/GA/jdk12.0.2/e482c34c86bd4bf8b56c0b35558996b9/10/GPL/openjdk-12.0.2_linux-x64_bin.tar.gz
|
||||
|
||||
RUN mkdir /usr/java
|
||||
|
||||
RUN mv openjdk-12.0.2_linux-x64_bin.tar.gz /usr/java
|
||||
|
||||
RUN cd /usr/java && tar -xzvf openjdk-12.0.2_linux-x64_bin.tar.gz
|
||||
|
||||
RUN echo 'JAVA_HOME=/usr/java/jdk-12.0.2' >> /etc/profile
|
||||
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
|
||||
|
||||
RUN apt-get install jq -y
|
||||
|
||||
RUN apt-get install curl -y
|
||||
|
||||
RUN apt-get install unzip -y
|
||||
|
||||
# Install OpenJDK 15.0.1
|
||||
RUN wget https://download.java.net/java/GA/jdk15.0.1/51f4f36ad4ef43e39d0dfdbaf6549e32/9/GPL/openjdk-15.0.1_linux-x64_bin.tar.gz
|
||||
# Install OpenJDK 21.0.7
|
||||
RUN wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz
|
||||
|
||||
RUN mv openjdk-15.0.1_linux-x64_bin.tar.gz /usr/java
|
||||
RUN mv OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz /usr/java
|
||||
RUN mkdir -p /usr/java/jdk-21.0.7
|
||||
RUN cd /usr/java && tar -xzvf OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz -C /usr/java/jdk-21.0.7
|
||||
|
||||
RUN cd /usr/java && tar -xzvf openjdk-15.0.1_linux-x64_bin.tar.gz
|
||||
|
||||
RUN echo 'JAVA_HOME=/usr/java/jdk-15.0.1' >> /etc/profile
|
||||
RUN echo 'JAVA_HOME=/usr/java/jdk-21.0.7' >> /etc/profile
|
||||
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
|
||||
RUN echo 'export JAVA_HOME' >> /etc/profile
|
||||
RUN echo 'export JRE_HOME' >> /etc/profile
|
||||
RUN echo 'export PATH' >> /etc/profile
|
||||
|
||||
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-12.0.2/bin/java" 1
|
||||
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-12.0.2/bin/javac" 1
|
||||
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-21.0.7/bin/java" 1
|
||||
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-21.0.7/bin/javac" 1
|
||||
|
||||
#install postgres 13
|
||||
# Import Repository Signing Key
|
||||
|
|
|
|||
|
|
@ -138,8 +138,8 @@ do
|
|||
cd supertokens-root
|
||||
rm gradle.properties
|
||||
|
||||
update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 2
|
||||
update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 2
|
||||
update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-21.0.7/bin/java" 2
|
||||
update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-21.0.7/bin/javac" 2
|
||||
|
||||
coreX=$(cut -d'.' -f1 <<<"$coreVersion")
|
||||
coreY=$(cut -d'.' -f2 <<<"$coreVersion")
|
||||
|
|
|
|||
|
|
@ -4,20 +4,22 @@ RUN apt-get update && apt-get upgrade -y
|
|||
RUN apt-get install build-essential -y
|
||||
RUN apt-get install -y git-core wget unzip jq curl
|
||||
|
||||
# Install OpenJDK 15.0.1
|
||||
RUN wget https://download.java.net/java/GA/jdk15.0.1/51f4f36ad4ef43e39d0dfdbaf6549e32/9/GPL/openjdk-15.0.1_linux-x64_bin.tar.gz
|
||||
RUN mkdir -p /usr/java
|
||||
RUN mv openjdk-15.0.1_linux-x64_bin.tar.gz /usr/java
|
||||
RUN cd /usr/java && tar -xzvf openjdk-15.0.1_linux-x64_bin.tar.gz
|
||||
# Install OpenJDK 21.0.7
|
||||
RUN wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz
|
||||
|
||||
RUN echo 'JAVA_HOME=/usr/java/jdk-15.0.1' >> /etc/profile
|
||||
RUN mv OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz /usr/java
|
||||
RUN mkdir -p /usr/java/
|
||||
RUN cd /usr/java && tar -xzvf OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz
|
||||
RUN mv /usr/java/jdk-21.0.7+6 /usr/java/jdk-21.0.7
|
||||
|
||||
RUN echo 'JAVA_HOME=/usr/java/jdk-21.0.7' >> /etc/profile
|
||||
RUN echo 'PATH=$PATH:$HOME/bin:$JAVA_HOME/bin' >> /etc/profile
|
||||
RUN echo 'export JAVA_HOME' >> /etc/profile
|
||||
RUN echo 'export JRE_HOME' >> /etc/profile
|
||||
RUN echo 'export PATH' >> /etc/profile
|
||||
|
||||
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 1
|
||||
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 1
|
||||
RUN update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-21.0.7/bin/java" 1
|
||||
RUN update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-21.0.7/bin/javac" 1
|
||||
|
||||
RUN wget -O docker-entrypoint.sh https://raw.githubusercontent.com/supertokens/supertokens-docker-postgresql/master/docker-entrypoint.sh
|
||||
|
||||
|
|
@ -51,7 +53,7 @@ RUN set -x \
|
|||
&& gpgconf --kill all \
|
||||
&& rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc \
|
||||
&& chmod +x /usr/local/bin/gosu \
|
||||
&& wget -O jre.zip "https://raw.githubusercontent.com/supertokens/jre/master/jre-15.0.1-linux.zip" \
|
||||
&& wget -O jre.zip "https://raw.githubusercontent.com/supertokens/jre/master/jre-21.0.7-linux.zip" \
|
||||
&& mkdir -p /usr/lib/supertokens/jre \
|
||||
&& unzip jre.zip \
|
||||
&& mv jre-*/* /usr/lib/supertokens/jre \
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
import http.client
|
||||
|
||||
def register_core_version(supertokens_api_key, core_version, plugin_interface_array, core_driver_array):
|
||||
print("Core Version: ", core_version)
|
||||
print("Plugin Interface Array: ", plugin_interface_array)
|
||||
print("Core Driver Array: ", core_driver_array)
|
||||
|
||||
conn = http.client.HTTPSConnection("api.supertokens.io")
|
||||
|
||||
payload = {
|
||||
"password": supertokens_api_key,
|
||||
"planType": "FREE",
|
||||
"version": core_version,
|
||||
"pluginInterfaces": plugin_interface_array,
|
||||
"coreDriverInterfaces": core_driver_array
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'api-version': '0'
|
||||
}
|
||||
|
||||
conn.request("PUT", "/0/core", json.dumps(payload), headers)
|
||||
response = conn.getresponse()
|
||||
|
||||
if response.status != 200:
|
||||
print(f"failed core PUT API status code: {response.status}. Exiting!")
|
||||
exit(1)
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
def read_core_version():
|
||||
with open('build.gradle', 'r') as file:
|
||||
for line in file:
|
||||
if 'version =' in line:
|
||||
return line.split('=')[1].strip().strip("'\"")
|
||||
raise Exception("Could not find version in build.gradle")
|
||||
|
||||
core_version = read_core_version()
|
||||
|
||||
with open('pluginInterfaceSupported.json', 'r') as fd:
|
||||
plugin_interface_array = json.load(fd)['versions']
|
||||
|
||||
with open('coreDriverInterfaceSupported.json', 'r') as fd:
|
||||
core_driver_array = json.load(fd)['versions']
|
||||
|
||||
register_core_version(
|
||||
supertokens_api_key=os.environ.get("SUPERTOKENS_API_KEY"),
|
||||
core_version=core_version,
|
||||
plugin_interface_array=plugin_interface_array,
|
||||
core_driver_array=core_driver_array
|
||||
)
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import http.client
|
||||
|
||||
|
||||
def register_plugin_version(supertokens_api_key, plugin_version, plugin_interface_array, plugin_name):
|
||||
print("Plugin Version: ", plugin_version)
|
||||
print("Plugin Interface Array: ", plugin_interface_array)
|
||||
print("Plugin Name: ", plugin_name)
|
||||
|
||||
conn = http.client.HTTPSConnection("api.supertokens.io")
|
||||
|
||||
payload = {
|
||||
"password": supertokens_api_key,
|
||||
"planType": "FREE",
|
||||
"version": plugin_version,
|
||||
"pluginInterfaces": plugin_interface_array,
|
||||
"name": plugin_name
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'api-version': '0'
|
||||
}
|
||||
|
||||
conn.request("PUT", "/0/plugin", json.dumps(payload), headers)
|
||||
response = conn.getresponse()
|
||||
|
||||
if response.status != 200:
|
||||
print(f"failed plugin PUT API status code: {response.status}. Exiting!")
|
||||
print(f"response: {str(response.read())}")
|
||||
exit(1)
|
||||
|
||||
conn.close()
|
||||
|
||||
def read_plugin_version():
|
||||
with open('build.gradle', 'r') as file:
|
||||
for line in file:
|
||||
if 'version =' in line:
|
||||
return line.split('=')[1].strip().strip("'\"")
|
||||
raise Exception("Could not find version in build.gradle")
|
||||
|
||||
plugin_version = read_plugin_version()
|
||||
|
||||
with open('pluginInterfaceSupported.json', 'r') as fd:
|
||||
plugin_interface_array = json.load(fd)['versions']
|
||||
|
||||
def check_if_tag_exists(tag):
|
||||
try:
|
||||
result = subprocess.run(['git', 'tag', '-l', tag], capture_output=True, text=True)
|
||||
return tag in result.stdout
|
||||
except subprocess.CalledProcessError:
|
||||
print(f"Error checking for tag {tag}")
|
||||
return False
|
||||
|
||||
dev_tag = f"dev-v{plugin_version}"
|
||||
if not check_if_tag_exists(dev_tag):
|
||||
print(f"Tag {dev_tag} does not exist. Exiting!")
|
||||
exit(0)
|
||||
|
||||
register_plugin_version(
|
||||
supertokens_api_key=os.environ.get("SUPERTOKENS_API_KEY"),
|
||||
plugin_version=plugin_version,
|
||||
plugin_interface_array=plugin_interface_array,
|
||||
plugin_name=os.environ.get("PLUGIN_NAME")
|
||||
)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Check for required arguments
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 <source-image:tag> <target-image:tag>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_IMAGE="$1"
|
||||
TARGET_IMAGE="$2"
|
||||
|
||||
# Platforms to support
|
||||
PLATFORMS=("linux/amd64" "linux/arm64")
|
||||
TEMP_IMAGES=()
|
||||
|
||||
# Pull, retag, and push platform-specific images
|
||||
for PLATFORM in "${PLATFORMS[@]}"; do
|
||||
ARCH=$(echo $PLATFORM | cut -d'/' -f2)
|
||||
TEMP_TAG="${TARGET_IMAGE}-${ARCH}"
|
||||
TEMP_IMAGES+=("$TEMP_TAG")
|
||||
|
||||
echo "Pulling $SOURCE_IMAGE for $PLATFORM..."
|
||||
docker pull --platform $PLATFORM "$SOURCE_IMAGE"
|
||||
|
||||
echo "Tagging as $TEMP_TAG..."
|
||||
docker tag "$SOURCE_IMAGE" "$TEMP_TAG"
|
||||
|
||||
echo "Pushing $TEMP_TAG..."
|
||||
docker push "$TEMP_TAG"
|
||||
done
|
||||
|
||||
# Create and push manifest for multi-arch image
|
||||
echo "Creating and pushing multi-arch manifest for $TARGET_IMAGE..."
|
||||
docker manifest create "$TARGET_IMAGE" "${TEMP_IMAGES[@]}"
|
||||
docker manifest push "$TARGET_IMAGE"
|
||||
|
||||
echo "✅ Multi-arch image pushed as $TARGET_IMAGE"
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import http.client
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
REPO = "supertokens/supertokens-core"
|
||||
SHA = os.environ.get("GITHUB_SHA")
|
||||
NAME = os.environ.get("WORKFLOW_NAME", "Publish Dev Docker Image")
|
||||
|
||||
st = time.time()
|
||||
|
||||
def get_latest_actions():
|
||||
conn = http.client.HTTPSConnection("api.github.com")
|
||||
url = f"/repos/{REPO}/actions/runs"
|
||||
headers = {"User-Agent": "Python-http.client"}
|
||||
conn.request("GET", url, headers=headers)
|
||||
response = conn.getresponse()
|
||||
|
||||
if response.status == 200:
|
||||
data = response.read()
|
||||
runs = json.loads(data)['workflow_runs']
|
||||
found = False
|
||||
for run in runs:
|
||||
if run['head_sha'] == SHA and run['name'] == NAME:
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
print("No matching workflow run found.")
|
||||
sys.exit(1)
|
||||
|
||||
if run["status"] == "completed":
|
||||
if run["conclusion"] == "success":
|
||||
print("Workflow completed successfully.")
|
||||
return True
|
||||
else:
|
||||
print(f"Workflow failed with conclusion: {run['conclusion']}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"Failed to fetch workflow runs: {response.status} {response.reason}")
|
||||
sys.exit(1)
|
||||
|
||||
return False
|
||||
|
||||
time.sleep(30) # Wait for 30 seconds before checking
|
||||
|
||||
|
||||
while not get_latest_actions():
|
||||
print("Waiting for the latest actions to complete...")
|
||||
time.sleep(10)
|
||||
if time.time() - st > 600:
|
||||
print("Timed out waiting for the latest actions.")
|
||||
sys.exit(1)
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
name: Add dev tags for release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
core-version:
|
||||
description: 'Core version'
|
||||
required: true
|
||||
type: string
|
||||
plugin-interface-version:
|
||||
description: 'Plugin interface version'
|
||||
required: true
|
||||
type: string
|
||||
new-release-for-plugin-interface:
|
||||
description: 'New release for plugin interface'
|
||||
required: true
|
||||
type: boolean
|
||||
postgresql-plugin-version:
|
||||
description: 'Postgres plugin version'
|
||||
required: true
|
||||
new-release-for-postgresql-plugin:
|
||||
description: 'New release for postgres plugin'
|
||||
required: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
dependency-branches:
|
||||
name: Dependency Branches
|
||||
environment: publish
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
branches: ${{ steps.result.outputs.branches }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: supertokens/get-core-dependencies-action@main
|
||||
id: result
|
||||
with:
|
||||
run-for: add-dev-tag
|
||||
core-version: ${{ github.event.inputs.core-version }}
|
||||
plugin-interface-version: ${{ github.event.inputs.plugin-interface-version }}
|
||||
postgresql-plugin-version: ${{ github.event.inputs.postgresql-plugin-version }}
|
||||
add-dev-tag:
|
||||
environment: publish
|
||||
runs-on: ubuntu-latest
|
||||
needs: dependency-branches
|
||||
steps:
|
||||
- name: Set up JDK 21.0.7
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 21.0.7
|
||||
distribution: zulu
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
repository: supertokens/supertokens-root
|
||||
path: ./supertokens-root
|
||||
ref: master
|
||||
- name: Checkout supertokens-core
|
||||
run: |
|
||||
cd supertokens-root
|
||||
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-core.git
|
||||
cd supertokens-core
|
||||
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['core'] }}
|
||||
- name: Checkout supertokens-plugin-interface
|
||||
run: |
|
||||
cd supertokens-root
|
||||
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-plugin-interface.git
|
||||
cd supertokens-plugin-interface
|
||||
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['plugin-interface'] }}
|
||||
- name: Checkout supertokens-postgresql-plugin
|
||||
run: |
|
||||
cd supertokens-root
|
||||
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-postgresql-plugin.git
|
||||
cd supertokens-postgresql-plugin
|
||||
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['postgresql'] }}
|
||||
- name: Load Modules
|
||||
run: |
|
||||
cd supertokens-root
|
||||
echo "core,master
|
||||
plugin-interface,master
|
||||
postgresql-plugin,master
|
||||
" > modules.txt
|
||||
cat modules.txt
|
||||
./loadModules
|
||||
- name: Setup test env
|
||||
run: cd supertokens-root && ./utils/setupTestEnv --local
|
||||
- name: Git config
|
||||
run: |
|
||||
git config --global user.name "Supertokens Bot"
|
||||
git config --global user.email "<>"
|
||||
- name: Add dev tag to plugin interface
|
||||
if: ${{ github.event.inputs.new-release-for-plugin-interface == 'true' }}
|
||||
run: |
|
||||
echo "Adding dev tag to plugin interface"
|
||||
cd supertokens-root/supertokens-plugin-interface
|
||||
./addDevTag
|
||||
- name: Add dev tag to postgres plugin
|
||||
if: ${{ github.event.inputs.new-release-for-postgresql-plugin == 'true' }}
|
||||
run: |
|
||||
echo "Adding dev tag to postgres plugin"
|
||||
cd supertokens-root/supertokens-postgresql-plugin
|
||||
./addDevTag
|
||||
- name: Add dev tag to core
|
||||
run: |
|
||||
echo "Adding dev tag to core"
|
||||
cd supertokens-root/supertokens-core
|
||||
./addDevTag
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
name: Container Security Scan
|
||||
|
||||
on:
|
||||
# Allow manual triggering
|
||||
workflow_dispatch:
|
||||
|
||||
# Run automatically once a day at 2 AM UTC
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
jobs:
|
||||
container-scan:
|
||||
name: Scan SuperTokens PostgreSQL Container
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Run Azure Container Scan
|
||||
id: container-scan
|
||||
uses: Azure/container-scan@v0
|
||||
continue-on-error: true
|
||||
with:
|
||||
image-name: supertokens/supertokens-postgresql:latest
|
||||
severity-threshold: LOW
|
||||
run-quality-checks: false
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST: 1
|
||||
|
||||
- name: Upload scan results
|
||||
id: upload-scan-results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: container-scan-results
|
||||
path: |
|
||||
${{ steps.container-scan.outputs.scan-report-path }}
|
||||
retention-days: 30
|
||||
|
||||
- name: Generate Security Summary
|
||||
id: security-summary
|
||||
run: |
|
||||
echo "summary<<EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "**Image:** \`supertokens/supertokens-postgresql:latest\`\n" >> $GITHUB_OUTPUT
|
||||
echo "**Scan Date:** \`$(date -u)\`\n" >> $GITHUB_OUTPUT
|
||||
echo "\n" >> $GITHUB_OUTPUT
|
||||
|
||||
# Get the scan report path from the container scan output
|
||||
SCAN_REPORT_PATH="${{ steps.container-scan.outputs.scan-report-path }}"
|
||||
|
||||
if [ -f "$SCAN_REPORT_PATH" ]; then
|
||||
# Count vulnerabilities by severity using the correct JSON structure
|
||||
critical=$(jq '[.vulnerabilities[]? | select(.severity == "CRITICAL")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
|
||||
high=$(jq '[.vulnerabilities[]? | select(.severity == "HIGH")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
|
||||
medium=$(jq '[.vulnerabilities[]? | select(.severity == "MEDIUM")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
|
||||
low=$(jq '[.vulnerabilities[]? | select(.severity == "LOW")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
|
||||
|
||||
total_vulns=$(jq '[.vulnerabilities[]?] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
|
||||
|
||||
echo "**Total Vulnerabilities:** $total_vulns\n" >> $GITHUB_OUTPUT
|
||||
echo "\n" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "- 🔴 **Critical**: $critical\n" >> $GITHUB_OUTPUT
|
||||
echo "- 🟠 **High**: $high\n" >> $GITHUB_OUTPUT
|
||||
echo "- 🟡 **Medium**: $medium\n" >> $GITHUB_OUTPUT
|
||||
echo "- 🟢 **Low**: $low\n" >> $GITHUB_OUTPUT
|
||||
echo "\n" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "❌ **Scan results not found or scan failed**" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
echo "\n" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "[📃 Download the full report](${{ steps.upload-scan-results.outputs.artifact-url }})\n" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Add to Action Summary
|
||||
run: |
|
||||
echo "**Image:** \`supertokens/supertokens-postgresql:latest\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Scan Date:** \`$(date -u)\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Get the scan report path from the container scan output
|
||||
SCAN_REPORT_PATH="${{ steps.container-scan.outputs.scan-report-path }}"
|
||||
|
||||
if [ -f "$SCAN_REPORT_PATH" ]; then
|
||||
# Count vulnerabilities by severity using the correct JSON structure
|
||||
critical=$(jq '[.vulnerabilities[]? | select(.severity == "CRITICAL")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
|
||||
high=$(jq '[.vulnerabilities[]? | select(.severity == "HIGH")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
|
||||
medium=$(jq '[.vulnerabilities[]? | select(.severity == "MEDIUM")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
|
||||
low=$(jq '[.vulnerabilities[]? | select(.severity == "LOW")] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
|
||||
|
||||
total_vulns=$(jq '[.vulnerabilities[]?] | length' "$SCAN_REPORT_PATH" 2>/dev/null || echo "0")
|
||||
|
||||
echo "**Total Vulnerabilities:** $total_vulns" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "- 🔴 **Critical**: $critical" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🟠 **High**: $high" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🟡 **Medium**: $medium" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🟢 **Low**: $low" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "**Vulnerabilities:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| ID | Package | Severity | | Description |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----|---------|----------|-|-------------|" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Extract and format vulnerabilities into a table with colored severity indicators, excluding LOW severity
|
||||
jq -r '.vulnerabilities[]? | select(.severity != "LOW") | "| \(.vulnerabilityId // "N/A") | \(.packageName // "N/A") | \(.severity // "UNKNOWN") | \(if .severity == "CRITICAL" then "🔴" elif .severity == "HIGH" then "🟠" elif .severity == "MEDIUM" then "🟡" else "🟢" end) | \((.description // "No description available") | gsub("\n"; " ")) |"' "$SCAN_REPORT_PATH" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "❌ **Scan results not found or scan failed**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[📃 Download the full report](${{ steps.upload-scan-results.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Post notification on Slack channel
|
||||
id: deployment_message
|
||||
uses: slackapi/slack-github-action@v2.1.0
|
||||
with:
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: ${{ secrets.SLACK_CHANNEL_ID }}
|
||||
text: ""
|
||||
blocks:
|
||||
- type: "header"
|
||||
text:
|
||||
type: "plain_text"
|
||||
text: "${{ steps.container-scan.outcome == 'success' && '✅' || '❌' }} Vulnerability Report: ${{ steps.container-scan.outcome == 'success' && 'All okay' || 'Needs attention' }}"
|
||||
- type: "markdown"
|
||||
text: "${{ steps.security-summary.outputs.summary }}"
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
name: Checks for release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '[0-9]+.[0-9]+'
|
||||
tags:
|
||||
- 'dev-*'
|
||||
|
||||
jobs:
|
||||
dependency-versions:
|
||||
name: Dependency Versions
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
versions: ${{ steps.result.outputs.versions }}
|
||||
branches: ${{ steps.result.outputs.branches }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: supertokens/get-core-dependencies-action@main
|
||||
with:
|
||||
run-for: PR
|
||||
id: result
|
||||
new-core-version:
|
||||
environment: publish
|
||||
name: New core version
|
||||
runs-on: ubuntu-latest
|
||||
needs: [dependency-versions]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Run script
|
||||
env:
|
||||
SUPERTOKENS_API_KEY: ${{ secrets.SUPERTOKENS_API_KEY }}
|
||||
run: |
|
||||
python .github/helpers/register-new-core-version.py
|
||||
new-plugin-versions:
|
||||
environment: publish
|
||||
name: New plugin versions
|
||||
runs-on: ubuntu-latest
|
||||
needs: [dependency-versions]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
plugin:
|
||||
- postgresql
|
||||
# no longer supported
|
||||
# - mysql
|
||||
# - mongodb
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: ./supertokens-plugin
|
||||
repository: supertokens/supertokens-${{ matrix.plugin }}-plugin
|
||||
ref: ${{ fromJson(needs.dependency-versions.outputs.branches)[matrix.plugin] }}
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
- name: Run script
|
||||
env:
|
||||
SUPERTOKENS_API_KEY: ${{ secrets.SUPERTOKENS_API_KEY }}
|
||||
PLUGIN_NAME: ${{ matrix.plugin }}
|
||||
run: |
|
||||
cd supertokens-plugin
|
||||
python ../.github/helpers/register-new-plugin-version.py
|
||||
unit-tests:
|
||||
name: Run unit tests
|
||||
needs: [new-core-version, new-plugin-versions]
|
||||
uses: ./.github/workflows/unit-test.yml
|
||||
wait-for-docker:
|
||||
name: Wait for Docker
|
||||
runs-on: ubuntu-latest
|
||||
needs: [new-core-version, new-plugin-versions]
|
||||
outputs:
|
||||
tag: ${{ steps.set_tag.outputs.TAG }}
|
||||
steps:
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Wait for Docker build
|
||||
env:
|
||||
SHA: ${{ github.sha }}
|
||||
run: |
|
||||
python .github/helpers/wait-for-docker.py
|
||||
- name: set tag
|
||||
id: set_tag
|
||||
run: |
|
||||
echo "TAG=${GITHUB_REF}" | sed 's/refs\/heads\///g' | sed 's/\//_/g' >> $GITHUB_OUTPUT
|
||||
stress-tests:
|
||||
needs: [wait-for-docker]
|
||||
uses: ./.github/workflows/stress-tests.yml
|
||||
with:
|
||||
tag: ${{ needs.wait-for-docker.outputs.tag }}
|
||||
mark-as-passed:
|
||||
environment: publish
|
||||
needs: [dependency-versions, unit-tests, stress-tests]
|
||||
name: Mark as passed
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
plugin:
|
||||
- sqlite
|
||||
- postgresql
|
||||
# no longer supported
|
||||
# - mysql
|
||||
# - mongodb
|
||||
steps:
|
||||
- name: Mark plugin as passed
|
||||
if: matrix.plugin != 'sqlite' && fromJson(needs.dependency-versions.outputs.versions)[matrix.plugin] != ''
|
||||
uses: muhfaris/request-action@main
|
||||
with:
|
||||
url: https://api.supertokens.io/0/plugin
|
||||
method: PATCH
|
||||
headers: |
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
"api-version": "0"
|
||||
}
|
||||
body: |
|
||||
{
|
||||
"password": "${{ secrets.SUPERTOKENS_API_KEY }}",
|
||||
"version": "${{ fromJson(needs.dependency-versions.outputs.versions)[matrix.plugin] }}",
|
||||
"planType": "FREE",
|
||||
"name": "${{ matrix.plugin }}",
|
||||
"testPassed": true
|
||||
}
|
||||
- name: Mark core as passed
|
||||
if: matrix.plugin == 'sqlite' && fromJson(needs.dependency-versions.outputs.versions)['core'] != ''
|
||||
uses: muhfaris/request-action@main
|
||||
with:
|
||||
url: https://api.supertokens.io/0/core
|
||||
method: PATCH
|
||||
headers: |
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
"api-version": "0"
|
||||
}
|
||||
body: |
|
||||
{
|
||||
"password": "${{ secrets.SUPERTOKENS_API_KEY }}",
|
||||
"version": "${{ fromJson(needs.dependency-versions.outputs.versions)['core'] }}",
|
||||
"planType": "FREE",
|
||||
"testPassed": true
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
name: Do Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
core-version:
|
||||
description: 'Core version'
|
||||
required: true
|
||||
type: string
|
||||
plugin-interface-version:
|
||||
description: 'Plugin interface version'
|
||||
required: true
|
||||
type: string
|
||||
new-release-for-plugin-interface:
|
||||
description: 'New release for plugin interface'
|
||||
required: true
|
||||
type: boolean
|
||||
postgresql-plugin-version:
|
||||
description: 'Postgres plugin version'
|
||||
required: true
|
||||
new-release-for-postgresql-plugin:
|
||||
description: 'New release for postgres plugin'
|
||||
required: true
|
||||
type: boolean
|
||||
is-latest-release:
|
||||
description: 'Is this the latest release?'
|
||||
required: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
dependency-branches:
|
||||
name: Dependency Branches
|
||||
environment: publish
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
branches: ${{ steps.result.outputs.branches }}
|
||||
versions: ${{ steps.result.outputs.versions }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: supertokens/get-core-dependencies-action@main
|
||||
id: result
|
||||
with:
|
||||
run-for: add-dev-tag
|
||||
core-version: ${{ github.event.inputs.core-version }}
|
||||
plugin-interface-version: ${{ github.event.inputs.plugin-interface-version }}
|
||||
postgresql-plugin-version: ${{ github.event.inputs.postgresql-plugin-version }}
|
||||
release-docker:
|
||||
environment: publish
|
||||
name: Release Docker
|
||||
runs-on: ubuntu-latest
|
||||
needs: dependency-branches
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up JDK 21.0.7
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 21.0.7
|
||||
distribution: zulu
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Tag and Push Docker Image
|
||||
run: |
|
||||
tag=${{ github.event.inputs.core-version }}
|
||||
major=$(echo $tag | cut -d. -f1)
|
||||
minor=$(echo $tag | cut -d. -f1,2)
|
||||
|
||||
bash .github/helpers/release-docker.sh supertokens/supertokens-dev-postgresql:$minor supertokens/supertokens-postgresql:$major
|
||||
bash .github/helpers/release-docker.sh supertokens/supertokens-dev-postgresql:$minor supertokens/supertokens-postgresql:$minor
|
||||
bash .github/helpers/release-docker.sh supertokens/supertokens-dev-postgresql:$minor supertokens/supertokens-postgresql:$tag
|
||||
|
||||
if [ "${{ github.event.inputs.is-latest-release }}" == "true" ]; then
|
||||
bash .github/helpers/release-docker.sh supertokens/supertokens-dev-postgresql:$minor supertokens/supertokens-postgresql:latest
|
||||
fi
|
||||
add-release-tag:
|
||||
environment: publish
|
||||
runs-on: ubuntu-latest
|
||||
needs: [dependency-branches, release-docker]
|
||||
steps:
|
||||
- name: Set up JDK 21.0.7
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 21.0.7
|
||||
distribution: zulu
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
repository: supertokens/supertokens-root
|
||||
path: ./supertokens-root
|
||||
ref: master
|
||||
- name: Checkout supertokens-core
|
||||
run: |
|
||||
cd supertokens-root
|
||||
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-core.git
|
||||
cd supertokens-core
|
||||
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['core'] }}
|
||||
- name: Checkout supertokens-plugin-interface
|
||||
run: |
|
||||
cd supertokens-root
|
||||
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-plugin-interface.git
|
||||
cd supertokens-plugin-interface
|
||||
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['plugin-interface'] }}
|
||||
- name: Checkout supertokens-postgresql-plugin
|
||||
run: |
|
||||
cd supertokens-root
|
||||
git clone https://${{ secrets.GH_TOKEN }}@github.com/supertokens/supertokens-postgresql-plugin.git
|
||||
cd supertokens-postgresql-plugin
|
||||
git checkout ${{ fromJson(needs.dependency-branches.outputs.branches)['postgresql'] }}
|
||||
- name: Add release password
|
||||
run: |
|
||||
cd supertokens-root
|
||||
echo "${{ secrets.SUPERTOKENS_API_KEY }}" > releasePassword
|
||||
echo "${{ secrets.SUPERTOKENS_API_KEY }}" > apiPassword
|
||||
- name: Load Modules
|
||||
run: |
|
||||
cd supertokens-root
|
||||
echo "core,master
|
||||
plugin-interface,master
|
||||
postgresql-plugin,master
|
||||
" > modules.txt
|
||||
cat modules.txt
|
||||
./loadModules
|
||||
- name: Setup test env
|
||||
run: cd supertokens-root && ./utils/setupTestEnv --local
|
||||
- name: Git config
|
||||
run: |
|
||||
git config --global user.name "Supertokens Bot"
|
||||
git config --global user.email "<>"
|
||||
- name: Add release tag to plugin interface
|
||||
if: ${{ github.event.inputs.new-release-for-plugin-interface == 'true' }}
|
||||
run: |
|
||||
echo "Adding release tag to plugin interface"
|
||||
cd supertokens-root/supertokens-plugin-interface
|
||||
./addReleaseTag
|
||||
- name: Add release tag to postgres plugin
|
||||
if: ${{ github.event.inputs.new-release-for-postgresql-plugin == 'true' }}
|
||||
run: |
|
||||
echo "Adding release tag to postgres plugin"
|
||||
cd supertokens-root/supertokens-postgresql-plugin
|
||||
./addReleaseTag
|
||||
- name: Add release tag to core
|
||||
run: |
|
||||
echo "Adding release tag to core"
|
||||
cd supertokens-root/supertokens-core
|
||||
./addReleaseTag
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
name: "Enforcing changelog in PRs Workflow"
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened, ready_for_review, labeled, unlabeled ]
|
||||
|
||||
jobs:
|
||||
# Enforces the update of a changelog file on every pull request
|
||||
changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: dangoslen/changelog-enforcer@v2
|
||||
with:
|
||||
changeLogPath: 'CHANGELOG.md'
|
||||
skipLabels: 'Skip-Changelog'
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
name: "Lint PR Title"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
pr-title:
|
||||
name: Lint PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
validateSingleCommit: true
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
name: PR Checks
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened, ready_for_review, labeled, unlabeled ]
|
||||
|
||||
jobs:
|
||||
pr-title:
|
||||
name: Lint PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
validateSingleCommit: true
|
||||
changelog:
|
||||
name: Enforce Changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: dangoslen/changelog-enforcer@v2
|
||||
with:
|
||||
changeLogPath: 'CHANGELOG.md'
|
||||
skipLabels: 'Skip-Changelog'
|
||||
unit-tests:
|
||||
name: Run unit tests
|
||||
uses: ./.github/workflows/unit-test.yml
|
||||
|
|
@ -3,10 +3,82 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- "**"
|
||||
tags:
|
||||
- 'dev-*'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
dependency-branches:
|
||||
name: Dependency Branches
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
branches: ${{ steps.result.outputs.branches }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: supertokens/get-core-dependencies-action@main
|
||||
id: result
|
||||
with:
|
||||
run-for: PR
|
||||
core-branch: ${{ github.ref_name }}
|
||||
|
||||
docker:
|
||||
name: Docker
|
||||
runs-on: ubuntu-latest
|
||||
needs: dependency-branches
|
||||
outputs:
|
||||
tag: ${{ steps.set_tag.outputs.TAG }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
plugin:
|
||||
- postgresql
|
||||
# no longer supported
|
||||
# - mysql
|
||||
# - mongodb
|
||||
steps:
|
||||
- name: Set up JDK 21.0.7
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 21.0.7
|
||||
distribution: zulu
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
repository: supertokens/supertokens-root
|
||||
path: ./supertokens-root
|
||||
ref: master
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
path: ./supertokens-root/supertokens-core
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
repository: supertokens/supertokens-plugin-interface
|
||||
path: ./supertokens-root/supertokens-plugin-interface
|
||||
ref: ${{ fromJson(needs.dependency-branches.outputs.branches)['plugin-interface'] }}
|
||||
- uses: actions/checkout@v2
|
||||
if: matrix.plugin != 'sqlite'
|
||||
with:
|
||||
repository: supertokens/supertokens-${{ matrix.plugin }}-plugin
|
||||
path: ./supertokens-root/supertokens-${{ matrix.plugin }}-plugin
|
||||
ref: ${{ fromJson(needs.dependency-branches.outputs.branches)[matrix.plugin] }}
|
||||
- name: Load Modules
|
||||
run: |
|
||||
cd supertokens-root
|
||||
echo "core,master
|
||||
plugin-interface,master
|
||||
${{ matrix.plugin }}-plugin,master
|
||||
" > modules.txt
|
||||
cat modules.txt
|
||||
./loadModules
|
||||
- name: Setup test env
|
||||
run: cd supertokens-root && ./utils/setupTestEnv --local
|
||||
- name: Generate config file
|
||||
run: |
|
||||
cd supertokens-root
|
||||
touch config_temp.yaml
|
||||
cat supertokens-core/config.yaml >> config_temp.yaml
|
||||
cat supertokens-${{ matrix.plugin }}-plugin/config.yaml >> config_temp.yaml
|
||||
mv config_temp.yaml config.yaml
|
||||
|
||||
- name: set tag
|
||||
id: set_tag
|
||||
run: |
|
||||
|
|
@ -17,26 +89,16 @@ jobs:
|
|||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# - name: Build and export to Docker
|
||||
# uses: docker/build-push-action@v6
|
||||
# with:
|
||||
# load: true
|
||||
# tags: ${{ env.TEST_TAG }}
|
||||
|
||||
# - name: Test
|
||||
# run: |
|
||||
# docker run --rm ${{ env.TEST_TAG }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: supertokens/supertokens-core:dev-branch-${{ steps.set_tag.outputs.TAG }}
|
||||
file: .github/helpers/Dockerfile
|
||||
context: ./supertokens-root
|
||||
tags: supertokens/supertokens-dev-${{ matrix.plugin }}:${{ steps.set_tag.outputs.TAG }}
|
||||
file: ./supertokens-root/supertokens-${{ matrix.plugin }}-plugin/.github/helpers/docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
name: Stress Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Docker image tag to use'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
stress-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd stress-tests
|
||||
npm install
|
||||
- name: Update Docker image in compose
|
||||
run: |
|
||||
cd stress-tests
|
||||
sed -i 's|supertokens/supertokens-postgresql|supertokens/supertokens-dev-postgresql:${{ inputs.tag }}|' docker-compose.yml
|
||||
cat docker-compose.yml
|
||||
- name: Bring up the services
|
||||
run: |
|
||||
cd stress-tests
|
||||
docker compose up -d
|
||||
- name: Generate user jsons
|
||||
run: |
|
||||
cd stress-tests
|
||||
npm run generate-users
|
||||
- name: Run one million users test
|
||||
id: one-million-users
|
||||
run: |
|
||||
cd stress-tests
|
||||
npm run one-million-users | tee stress-tests.log
|
||||
- name: Display Test Statistics
|
||||
run: |
|
||||
echo "## Stress Test Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Test | Duration |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|----------|" >> $GITHUB_STEP_SUMMARY
|
||||
jq -r '.measurements[] | "| \(.title) | \(.formatted) |"' stress-tests/stats.json >> $GITHUB_STEP_SUMMARY
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
name: "Check if \"Run tests\" action succeeded"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
pr-run-test-action:
|
||||
name: Check if "Run tests" action succeeded
|
||||
timeout-minutes: 60
|
||||
concurrency:
|
||||
group: ${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: node install
|
||||
run: cd ./.github/helpers && npm i
|
||||
- name: Calling github API
|
||||
run: cd ./.github/helpers && GITHUB_TOKEN=${{ github.token }} REPO=${{ github.repository }} RUN_ID=${{ github.run_id }} BRANCH=${{ github.head_ref }} JOB_ID=${{ github.job }} SOURCE_OWNER=${{ github.event.pull_request.head.repo.owner.login }} CURRENT_SHA=${{ github.event.pull_request.head.sha }} node node_modules/github-workflow-helpers/test-pass-check-pr.js
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
name: "Run tests"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pluginRepoOwnerName:
|
||||
description: 'supertokens-plugin-interface repo owner name'
|
||||
default: supertokens
|
||||
required: true
|
||||
pluginInterfaceBranch:
|
||||
description: 'supertokens-plugin-interface repos branch name'
|
||||
default: master
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
test_job:
|
||||
name: Run tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
container: rishabhpoddar/supertokens_core_testing
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cloning supertokens-root
|
||||
run: cd ../ && git clone https://github.com/supertokens/supertokens-root.git
|
||||
- name: Update Java 1
|
||||
run: update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 2
|
||||
- name: Update Java 2
|
||||
run: update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 2
|
||||
- name: Modifying modules.txt in supertokens-root
|
||||
run: cd ../supertokens-root && echo "core,master\nplugin-interface,${{ github.event.inputs.pluginInterfaceBranch }},${{ github.event.inputs.pluginRepoOwnerName }}" > modules.txt
|
||||
- name: Contents of modules.txt
|
||||
run: cat ../supertokens-root/modules.txt
|
||||
- name: Running loadModules in supertokens-root
|
||||
run: cd ../supertokens-root && ./loadModules
|
||||
- name: Copying current supertokens-core branch into supertokens-root
|
||||
run: cd ../supertokens-root && rm -rf ./supertokens-core && cp -r ../supertokens-core ./
|
||||
- name: Building and running tests
|
||||
run: cd ../supertokens-root && ./startTestingEnv
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
name: Unit Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
dependency-branches:
|
||||
name: Dependency Branches
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
branches: ${{ steps.result.outputs.branches }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: supertokens/get-core-dependencies-action@main
|
||||
id: result
|
||||
with:
|
||||
run-for: PR
|
||||
core-branch: ${{ github.head_ref }}
|
||||
|
||||
test:
|
||||
name: Unit tests
|
||||
needs: dependency-branches
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
plugin:
|
||||
- sqlite
|
||||
- postgresql
|
||||
# no longer supported
|
||||
# - mysql
|
||||
# - mongodb
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Set up JDK 21.0.7
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 21.0.7
|
||||
distribution: zulu
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
repository: supertokens/supertokens-root
|
||||
path: ./supertokens-root
|
||||
ref: master
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
path: ./supertokens-root/supertokens-core
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
repository: supertokens/supertokens-plugin-interface
|
||||
path: ./supertokens-root/supertokens-plugin-interface
|
||||
ref: ${{ fromJson(needs.dependency-branches.outputs.branches)['plugin-interface'] }}
|
||||
- uses: actions/checkout@v2
|
||||
if: matrix.plugin != 'sqlite'
|
||||
with:
|
||||
repository: supertokens/supertokens-${{ matrix.plugin }}-plugin
|
||||
path: ./supertokens-root/supertokens-${{ matrix.plugin }}-plugin
|
||||
ref: ${{ fromJson(needs.dependency-branches.outputs.branches)[matrix.plugin] }}
|
||||
- name: Load Modules
|
||||
run: |
|
||||
cd supertokens-root
|
||||
echo "core,master
|
||||
plugin-interface,master
|
||||
${{ matrix.plugin }}-plugin,master
|
||||
" > modules.txt
|
||||
cat modules.txt
|
||||
./loadModules
|
||||
- name: Setup test env
|
||||
run: cd supertokens-root && ./utils/setupTestEnv --local
|
||||
- name: Start ${{ matrix.plugin }} server
|
||||
if: matrix.plugin != 'sqlite'
|
||||
run: cd supertokens-root/supertokens-${{ matrix.plugin }}-plugin && ./startDb.sh
|
||||
- name: Run tests
|
||||
env:
|
||||
ST_PLUGIN_NAME: ${{ matrix.plugin }}
|
||||
run: |
|
||||
cd supertokens-root
|
||||
./gradlew test
|
||||
- name: Publish Test Report
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
if: always()
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
detailed_summary: true
|
||||
include_passed: false
|
||||
annotate_notice: true
|
||||
|
|
@ -12,6 +12,7 @@ gradle-app.setting
|
|||
!cli/jar/**/*.jar
|
||||
!downloader/jar/**/*.jar
|
||||
!ee/jar/**/*.jar
|
||||
!src/main/resources/**/*.jar
|
||||
|
||||
*target*
|
||||
*.war
|
||||
|
|
@ -47,4 +48,7 @@ local.properties
|
|||
*.iml
|
||||
ee/bin
|
||||
addDevTag
|
||||
addReleaseTag
|
||||
addReleaseTag
|
||||
|
||||
install-linux.sh
|
||||
install-windows.bat
|
||||
|
|
|
|||
335
CHANGELOG.md
335
CHANGELOG.md
|
|
@ -7,6 +7,341 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [11.3.0]
|
||||
|
||||
- Adds SAML features
|
||||
- Fixes potential deadlock issue with `TelemetryProvider`
|
||||
- Adds DeadlockLogger as an utility for discovering deadlock issues
|
||||
|
||||
### Migration
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS saml_clients (
|
||||
app_id VARCHAR(64) NOT NULL DEFAULT 'public',
|
||||
tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',
|
||||
client_id VARCHAR(256) NOT NULL,
|
||||
client_secret TEXT,
|
||||
sso_login_url TEXT NOT NULL,
|
||||
redirect_uris TEXT NOT NULL,
|
||||
default_redirect_uri TEXT NOT NULL,
|
||||
idp_entity_id VARCHAR(256) NOT NULL,
|
||||
idp_signing_certificate TEXT NOT NULL,
|
||||
allow_idp_initiated_login BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
enable_request_signing BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
CONSTRAINT saml_clients_pkey PRIMARY KEY(app_id, tenant_id, client_id),
|
||||
CONSTRAINT saml_clients_idp_entity_id_key UNIQUE (app_id, tenant_id, idp_entity_id),
|
||||
CONSTRAINT saml_clients_app_id_fkey FOREIGN KEY(app_id) REFERENCES apps (app_id) ON DELETE CASCADE,
|
||||
CONSTRAINT saml_clients_tenant_id_fkey FOREIGN KEY(app_id, tenant_id) REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS saml_clients_app_id_tenant_id_index ON saml_clients (app_id, tenant_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS saml_relay_state (
|
||||
app_id VARCHAR(64) NOT NULL DEFAULT 'public',
|
||||
tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',
|
||||
relay_state VARCHAR(256) NOT NULL,
|
||||
client_id VARCHAR(256) NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
redirect_uri TEXT NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
CONSTRAINT saml_relay_state_pkey PRIMARY KEY(app_id, tenant_id, relay_state),
|
||||
CONSTRAINT saml_relay_state_app_id_fkey FOREIGN KEY(app_id) REFERENCES apps (app_id) ON DELETE CASCADE,
|
||||
CONSTRAINT saml_relay_state_tenant_id_fkey FOREIGN KEY(app_id, tenant_id) REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS saml_relay_state_app_id_tenant_id_index ON saml_relay_state (app_id, tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS saml_relay_state_expires_at_index ON saml_relay_state (expires_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS saml_claims (
|
||||
app_id VARCHAR(64) NOT NULL DEFAULT 'public',
|
||||
tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',
|
||||
client_id VARCHAR(256) NOT NULL,
|
||||
code VARCHAR(256) NOT NULL,
|
||||
claims TEXT NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
CONSTRAINT saml_claims_pkey PRIMARY KEY(app_id, tenant_id, code),
|
||||
CONSTRAINT saml_claims_app_id_fkey FOREIGN KEY(app_id) REFERENCES apps (app_id) ON DELETE CASCADE,
|
||||
CONSTRAINT saml_claims_tenant_id_fkey FOREIGN KEY(app_id, tenant_id) REFERENCES tenants (app_id, tenant_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS saml_claims_app_id_tenant_id_index ON saml_claims (app_id, tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS saml_claims_expires_at_index ON saml_claims (expires_at);
|
||||
```
|
||||
|
||||
## [11.2.1]
|
||||
|
||||
- Fixes deadlock issue with `ResourceDistributor`
|
||||
- Fixes race issues with Refreshing OAuth token
|
||||
|
||||
## [11.2.0]
|
||||
|
||||
- Adds opentelemetry-javaagent to the core distribution
|
||||
|
||||
## [11.1.1]
|
||||
|
||||
- Updates tomcat-embed to 11.0.12 because of security vulnerabilities
|
||||
|
||||
## [11.1.0]
|
||||
|
||||
- Adds hikari logs to opentelemetry
|
||||
- Fetches core and plugin config from env
|
||||
- Open Telemetry configuration is now optional
|
||||
- Migrates API calls from supertokens.io to supertokens.com
|
||||
|
||||
## [11.0.5]
|
||||
|
||||
- Adds all logs to telemetry which were logged with `io/supertokens/output/Logging.java`
|
||||
- Upgrades the embedded tomcat to 11.0.8 because of security vulnerabilities
|
||||
- Adds back previously removed `implementationDependencies.json`, but now it is generated by the build process
|
||||
|
||||
## [11.0.4]
|
||||
|
||||
- Fixes user to roles association in bulk import users when the user is not a primary user
|
||||
|
||||
## [11.0.3]
|
||||
|
||||
- Fixes BatchUpdateException checks and error handling to prevent bulk import users stuck in `PROCESSING` state
|
||||
- Adds more DEBUG logging to the bulk import users process
|
||||
|
||||
## [11.0.2]
|
||||
|
||||
- Fixes `AuthRecipe#getUserByAccountInfo` to consider the tenantId instead of the appId when fetching the webauthn user
|
||||
|
||||
## [11.0.1]
|
||||
|
||||
- Upgrades the embedded tomcat 11.0.6 and logback classic to 1.5.13 because of security vulnerabilities
|
||||
|
||||
## [11.0.0]
|
||||
|
||||
- Migrates tests to Github Actions
|
||||
- Updates JRE to 21.
|
||||
|
||||
## [10.1.4]
|
||||
|
||||
- Fixes bulk migration user roles association when there is no external userId assigned to the user
|
||||
- Bulk migration now actually uses the `isVerified` field's value in the loginMethod input
|
||||
- Fixes nullpointer exception in bulk migration error handling in case of null external user id
|
||||
|
||||
## [10.1.3]
|
||||
|
||||
- Version bumped for re-release
|
||||
|
||||
## [10.1.2]
|
||||
|
||||
- Adds user_id index to the user roles table
|
||||
- Adds more debug logging to bulk migration
|
||||
- Adds more tests to bulk migration
|
||||
|
||||
### Migration
|
||||
|
||||
If using PostgreSQL, run the following SQL script:
|
||||
|
||||
```sql
|
||||
CREATE INDEX IF NOT EXISTS user_roles_app_id_user_id_index ON user_roles (app_id, user_id);
|
||||
```
|
||||
|
||||
If using MySQL, run the following SQL script:
|
||||
```sql
|
||||
CREATE INDEX user_roles_app_id_user_id_index ON user_roles (app_id, user_id);
|
||||
```
|
||||
|
||||
|
||||
## [10.1.1]
|
||||
|
||||
- Adds debug logging for the bulk migration process
|
||||
- Bulk migration users upload now returns the ids of the users.
|
||||
- Bulk Migration now requires Account Linking to be enabled only if the input data justifies it
|
||||
- Speed up Bulk Migration's account linking and primary user making
|
||||
|
||||
## [10.1.0]
|
||||
|
||||
- Adds Webauthn (Passkeys) support to core
|
||||
- Adds APIs:
|
||||
- GET `/recipe/webauthn/user/credential/`
|
||||
- GET `/recipe/webauthn/user/credential/list`
|
||||
- GET `/recipe/webauthn/options`
|
||||
- GET `/recipe/webauthn/user/recover`
|
||||
- POST `/recipe/webauthn/options/register`
|
||||
- POST `/recipe/webauthn/options/signin`
|
||||
- POST `/recipe/webauthn/user/credential/register`
|
||||
- POST `/recipe/webauthn/signup`
|
||||
- POST `/recipe/webauthn/signin`
|
||||
- POST `/recipe/webauthn/user/recover/token`
|
||||
- POST `/recipe/webauthn/user/recover/token/consume`
|
||||
- PUT `/recipe/webauthn/user/email`
|
||||
- DELETE `/recipe/webauthn/user/credential/remove`
|
||||
- DELETE `/recipe/webauthn/options/remove`
|
||||
- Adds additional indexing for `emailverification_verified_emails`
|
||||
- Introduces `bulk_migration_batch_size` core config
|
||||
- Introduces `BULK_MIGRATION_CRON_ENABLED` environment variable to control the bulk migration cron job
|
||||
|
||||
### Migration
|
||||
|
||||
If using PostgreSQL, run the following SQL script:
|
||||
|
||||
```sql
|
||||
|
||||
CREATE INDEX IF NOT EXISTS emailverification_verified_emails_app_id_email_index ON emailverification_verified_emails
|
||||
(app_id, email);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webauthn_account_recovery_tokens (
|
||||
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
email VARCHAR(256) NOT NULL,
|
||||
token VARCHAR(256) NOT NULL,
|
||||
expires_at BIGINT NOT NULL,
|
||||
CONSTRAINT webauthn_account_recovery_token_pkey PRIMARY KEY (app_id, tenant_id, user_id, token),
|
||||
CONSTRAINT webauthn_account_recovery_token_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES
|
||||
all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webauthn_credentials (
|
||||
id VARCHAR(256) NOT NULL,
|
||||
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
rp_id VARCHAR(256) NOT NULL,
|
||||
user_id CHAR(36),
|
||||
counter BIGINT NOT NULL,
|
||||
public_key BYTEA NOT NULL,
|
||||
transports TEXT NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
CONSTRAINT webauthn_credentials_pkey PRIMARY KEY (app_id, rp_id, id),
|
||||
CONSTRAINT webauthn_credentials_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES webauthn_users
|
||||
(app_id, user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webauthn_generated_options (
|
||||
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
tenant_id VARCHAR(64) DEFAULT 'public'NOT NULL,
|
||||
id CHAR(36) NOT NULL,
|
||||
challenge VARCHAR(256) NOT NULL,
|
||||
email VARCHAR(256),
|
||||
rp_id VARCHAR(256) NOT NULL,
|
||||
rp_name VARCHAR(256) NOT NULL,
|
||||
origin VARCHAR(256) NOT NULL,
|
||||
expires_at BIGINT NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
user_presence_required BOOLEAN DEFAULT false NOT NULL,
|
||||
user_verification VARCHAR(12) DEFAULT 'preferred' NOT NULL,
|
||||
CONSTRAINT webauthn_generated_options_pkey PRIMARY KEY (app_id, tenant_id, id),
|
||||
CONSTRAINT webauthn_generated_options_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES tenants
|
||||
(app_id, tenant_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webauthn_user_to_tenant (
|
||||
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
email VARCHAR(256) NOT NULL,
|
||||
CONSTRAINT webauthn_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email),
|
||||
CONSTRAINT webauthn_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id),
|
||||
CONSTRAINT webauthn_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES
|
||||
all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webauthn_users (
|
||||
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
email VARCHAR(256) NOT NULL,
|
||||
rp_id VARCHAR(256) NOT NULL,
|
||||
time_joined BIGINT NOT NULL,
|
||||
CONSTRAINT webauthn_users_pkey PRIMARY KEY (app_id, user_id),
|
||||
CONSTRAINT webauthn_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES app_id_to_user_id(app_id,
|
||||
user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS webauthn_user_to_tenant_email_index ON webauthn_user_to_tenant (app_id, email);
|
||||
CREATE INDEX IF NOT EXISTS webauthn_user_challenges_expires_at_index ON webauthn_generated_options (app_id, tenant_id, expires_at);
|
||||
CREATE INDEX IF NOT EXISTS webauthn_credentials_user_id_index ON webauthn_credentials (user_id);
|
||||
CREATE INDEX IF NOT EXISTS webauthn_account_recovery_token_token_index ON webauthn_account_recovery_tokens (app_id, tenant_id, token);
|
||||
CREATE INDEX IF NOT EXISTS webauthn_account_recovery_token_expires_at_index ON webauthn_account_recovery_tokens (expires_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS webauthn_account_recovery_token_email_index ON webauthn_account_recovery_tokens (app_id, tenant_id, email);
|
||||
```
|
||||
|
||||
If using MySQL, run the following SQL script:
|
||||
|
||||
```sql
|
||||
CREATE INDEX emailverification_verified_emails_app_id_email_index ON emailverification_verified_emails
|
||||
(app_id, email);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webauthn_account_recovery_tokens (
|
||||
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
email VARCHAR(256) NOT NULL,
|
||||
token VARCHAR(256) NOT NULL,
|
||||
expires_at BIGINT NOT NULL,
|
||||
CONSTRAINT webauthn_account_recovery_token_pkey PRIMARY KEY (app_id, tenant_id, user_id, token),
|
||||
CONSTRAINT webauthn_account_recovery_token_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES
|
||||
all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webauthn_credentials (
|
||||
id VARCHAR(256) NOT NULL,
|
||||
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
rp_id VARCHAR(256) NOT NULL,
|
||||
user_id CHAR(36),
|
||||
counter BIGINT NOT NULL,
|
||||
public_key BLOB NOT NULL,
|
||||
transports TEXT NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
CONSTRAINT webauthn_credentials_pkey PRIMARY KEY (app_id, rp_id, id),
|
||||
CONSTRAINT webauthn_credentials_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES webauthn_users
|
||||
(app_id, user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webauthn_generated_options (
|
||||
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
tenant_id VARCHAR(64) DEFAULT 'public'NOT NULL,
|
||||
id CHAR(36) NOT NULL,
|
||||
challenge VARCHAR(256) NOT NULL,
|
||||
email VARCHAR(256),
|
||||
rp_id VARCHAR(256) NOT NULL,
|
||||
rp_name VARCHAR(256) NOT NULL,
|
||||
origin VARCHAR(256) NOT NULL,
|
||||
expires_at BIGINT NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
user_presence_required BOOLEAN DEFAULT false NOT NULL,
|
||||
user_verification VARCHAR(12) DEFAULT 'preferred' NOT NULL,
|
||||
CONSTRAINT webauthn_generated_options_pkey PRIMARY KEY (app_id, tenant_id, id),
|
||||
CONSTRAINT webauthn_generated_options_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES tenants
|
||||
(app_id, tenant_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webauthn_user_to_tenant (
|
||||
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
email VARCHAR(256) NOT NULL,
|
||||
CONSTRAINT webauthn_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email),
|
||||
CONSTRAINT webauthn_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id),
|
||||
CONSTRAINT webauthn_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES
|
||||
all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webauthn_users (
|
||||
app_id VARCHAR(64) DEFAULT 'public' NOT NULL,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
email VARCHAR(256) NOT NULL,
|
||||
rp_id VARCHAR(256) NOT NULL,
|
||||
time_joined BIGINT NOT NULL,
|
||||
CONSTRAINT webauthn_users_pkey PRIMARY KEY (app_id, user_id),
|
||||
CONSTRAINT webauthn_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES app_id_to_user_id (app_id,
|
||||
user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX webauthn_user_to_tenant_email_index ON webauthn_user_to_tenant (app_id, email);
|
||||
CREATE INDEX webauthn_user_challenges_expires_at_index ON webauthn_generated_options (app_id, tenant_id, expires_at);
|
||||
CREATE INDEX webauthn_credentials_user_id_index ON webauthn_credentials (user_id);
|
||||
CREATE INDEX webauthn_account_recovery_token_token_index ON webauthn_account_recovery_tokens (app_id, tenant_id, token);
|
||||
CREATE INDEX webauthn_account_recovery_token_expires_at_index ON webauthn_account_recovery_tokens (expires_at DESC);
|
||||
CREATE INDEX webauthn_account_recovery_token_email_index ON webauthn_account_recovery_tokens (app_id, tenant_id, email);
|
||||
```
|
||||
|
||||
## [10.0.3]
|
||||
|
||||
- Fixes `StorageTransactionLogicException` in bulk import when not using userRoles and totpDevices in import json.
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ We're happy to help!:raised_hands:
|
|||
### Local Setup Prerequisites
|
||||
|
||||
- OS: Linux or macOS. Or if using Windows, you need to use [wsl2](https://docs.microsoft.com/en-us/windows/wsl/about).
|
||||
- JDK: openjdk 15.0.1. Installation instructions for Mac and Linux can be found
|
||||
- JDK: openjdk 21.0.7. Installation instructions for Mac and Linux can be found
|
||||
in [our wiki](https://github.com/supertokens/supertokens-core/wiki/Installing-OpenJDK-for-Mac-and-Linux)
|
||||
- IDE: [IntelliJ](https://www.jetbrains.com/idea/download/)(recommended) or equivalent IDE
|
||||
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ If you think this is a project you could use in the future, please :star2: this
|
|||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/Lehoczky"><img src="https://avatars.githubusercontent.com/u/31937175?v=4" width="100px;" alt=""/><br /><sub><b>Lehoczky Zoltán</b></sub></a></td>
|
||||
<td align="center"><a href="https://github.com/virajkanwade"><img src="https://avatars.githubusercontent.com/u/316111?v=4" width="100px;" alt=""/><br /><sub><b>Viraj Kanwade</b></sub></a></td>
|
||||
<td align="center"><a href="https://github.com/mavwolverine"><img src="https://avatars.githubusercontent.com/u/316111?v=4" width="100px;" alt=""/><br /><sub><b>Viraj Kanwade</b></sub></a></td>
|
||||
<td align="center"><a href="https://github.com/anuragmerndev"><img src="https://avatars.githubusercontent.com/u/144275260?v=4" width="100px;" alt=""/><br /><sub><b>Anurag Srivastava</b></sub></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
87
build.gradle
87
build.gradle
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'java-library'
|
||||
id "io.freefair.aspectj" version "8.13" //same as gradle version!
|
||||
}
|
||||
compileJava { options.encoding = "UTF-8" }
|
||||
compileTestJava { options.encoding = "UTF-8" }
|
||||
|
|
@ -19,29 +21,37 @@ compileTestJava { options.encoding = "UTF-8" }
|
|||
// }
|
||||
//}
|
||||
|
||||
version = "10.0.3"
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(21))
|
||||
}
|
||||
}
|
||||
|
||||
version = "11.3.0"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
maven { url 'https://build.shibboleth.net/nexus/content/repositories/releases/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// https://mvnrepository.com/artifact/com.google.code.gson/gson
|
||||
// if this changes, remember to also change in the ee folder's build.gradle
|
||||
implementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
|
||||
implementation group: 'com.google.code.gson', name: 'gson', version: '2.13.1'
|
||||
|
||||
// https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml
|
||||
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1'
|
||||
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.18.2'
|
||||
|
||||
// https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-cbor
|
||||
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-cbor', version: '2.18.2'
|
||||
|
||||
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
|
||||
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1'
|
||||
|
||||
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
|
||||
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14'
|
||||
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.2'
|
||||
|
||||
// https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core
|
||||
implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18'
|
||||
api group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '11.0.12'
|
||||
|
||||
// https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305
|
||||
implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
|
||||
|
|
@ -73,10 +83,36 @@ dependencies {
|
|||
// https://mvnrepository.com/artifact/com.googlecode.libphonenumber/libphonenumber/
|
||||
implementation group: 'com.googlecode.libphonenumber', name: 'libphonenumber', version: '8.13.25'
|
||||
|
||||
// https://mvnrepository.com/artifact/com.webauthn4j/webauthn4j-core
|
||||
implementation group: 'com.webauthn4j', name: 'webauthn4j-core', version: '0.28.6.RELEASE'
|
||||
|
||||
implementation platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha:2.17.0-alpha")
|
||||
|
||||
// Open SAML
|
||||
implementation group: 'org.opensaml', name: 'opensaml-core', version: '4.3.1'
|
||||
implementation group: 'org.opensaml', name: 'opensaml-saml-impl', version: '4.3.1'
|
||||
implementation group: 'org.opensaml', name: 'opensaml-security-impl', version: '4.3.1'
|
||||
implementation group: 'org.opensaml', name: 'opensaml-profile-impl', version: '4.3.1'
|
||||
implementation group: 'org.opensaml', name: 'opensaml-xmlsec-impl', version: '4.3.1'
|
||||
|
||||
implementation("ch.qos.logback:logback-core:1.5.18")
|
||||
implementation("ch.qos.logback:logback-classic:1.5.18")
|
||||
|
||||
// OpenTelemetry core
|
||||
implementation("io.opentelemetry:opentelemetry-sdk")
|
||||
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
|
||||
implementation("io.opentelemetry:opentelemetry-exporter-logging")
|
||||
implementation("io.opentelemetry:opentelemetry-api")
|
||||
|
||||
implementation("io.opentelemetry.semconv:opentelemetry-semconv")
|
||||
|
||||
implementation('org.aspectj:aspectjrt:1.9.24')
|
||||
|
||||
compileOnly project(":supertokens-plugin-interface")
|
||||
testImplementation project(":supertokens-plugin-interface")
|
||||
|
||||
// this is so that we can find plugin-interface jar while testing
|
||||
testImplementation project(":supertokens-plugin-interface")
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
|
||||
// https://mvnrepository.com/artifact/org.mockito/mockito-core
|
||||
|
|
@ -87,8 +123,9 @@ dependencies {
|
|||
|
||||
testImplementation 'com.tngtech.archunit:archunit-junit4:0.22.0'
|
||||
|
||||
// https://mvnrepository.com/artifact/com.webauthn4j/webauthn4j-test
|
||||
testImplementation group: 'com.webauthn4j', name: 'webauthn4j-test', version: '0.28.6.RELEASE'
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("io.supertokens.Main")
|
||||
}
|
||||
|
|
@ -98,43 +135,47 @@ jar {
|
|||
}
|
||||
|
||||
|
||||
task copyJars(type: Copy) {
|
||||
into "$buildDir/dependencies"
|
||||
tasks.register('copyJars', Copy) {
|
||||
from configurations.runtimeClasspath
|
||||
into layout.buildDirectory.dir("dependencies")
|
||||
}
|
||||
|
||||
test {
|
||||
jvmArgs '-Djava.security.egd=file:/dev/urandom'
|
||||
jvmArgs = ['-Djava.security.egd=file:/dev/urandom',
|
||||
"--add-opens=java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens=java.base/java.util=ALL-UNNAMED",
|
||||
"--add-opens=java.base/java.util.concurrent=ALL-UNNAMED"]
|
||||
testLogging {
|
||||
outputs.upToDateWhen { false }
|
||||
showStandardStreams = true
|
||||
}
|
||||
maxParallelForks = Runtime.runtime.availableProcessors()
|
||||
}
|
||||
|
||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
|
||||
tasks.withType(Test) {
|
||||
tasks.withType(Test).configureEach {
|
||||
testLogging {
|
||||
// set options for log level LIFECYCLE
|
||||
events TestLogEvent.FAILED,
|
||||
events = [TestLogEvent.FAILED,
|
||||
TestLogEvent.PASSED,
|
||||
TestLogEvent.SKIPPED,
|
||||
TestLogEvent.STANDARD_OUT
|
||||
exceptionFormat TestExceptionFormat.FULL
|
||||
showExceptions true
|
||||
showCauses true
|
||||
showStackTraces true
|
||||
TestLogEvent.STANDARD_OUT]
|
||||
exceptionFormat = TestExceptionFormat.FULL
|
||||
showExceptions = true
|
||||
showCauses = true
|
||||
showStackTraces = true
|
||||
|
||||
// set options for log level DEBUG and INFO
|
||||
debug {
|
||||
events TestLogEvent.STARTED,
|
||||
events = [TestLogEvent.STARTED,
|
||||
TestLogEvent.FAILED,
|
||||
TestLogEvent.PASSED,
|
||||
TestLogEvent.SKIPPED,
|
||||
TestLogEvent.STANDARD_ERROR,
|
||||
TestLogEvent.STANDARD_OUT
|
||||
exceptionFormat TestExceptionFormat.FULL
|
||||
TestLogEvent.STANDARD_OUT]
|
||||
exceptionFormat = TestExceptionFormat.FULL
|
||||
}
|
||||
info.events = debug.events
|
||||
info.exceptionFormat = debug.exceptionFormat
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ plugins {
|
|||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
maven { url 'https://build.shibboleth.net/nexus/content/repositories/releases/' }
|
||||
}
|
||||
|
||||
application {
|
||||
|
|
@ -16,13 +18,13 @@ jar {
|
|||
|
||||
dependencies {
|
||||
// https://mvnrepository.com/artifact/com.google.code.gson/gson
|
||||
implementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
|
||||
implementation group: 'com.google.code.gson', name: 'gson', version: '2.13.1'
|
||||
|
||||
// https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml
|
||||
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1'
|
||||
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.18.2'
|
||||
|
||||
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
|
||||
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1'
|
||||
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.2'
|
||||
|
||||
// https://mvnrepository.com/artifact/de.mkammerer/argon2-jvm
|
||||
implementation group: 'de.mkammerer', name: 'argon2-jvm', version: '2.11'
|
||||
|
|
@ -33,9 +35,9 @@ dependencies {
|
|||
testImplementation group: 'junit', name: 'junit', version: '4.12'
|
||||
}
|
||||
|
||||
task copyJars(type: Copy) {
|
||||
into "$buildDir/dependencies"
|
||||
tasks.register('copyJars', Copy) {
|
||||
from configurations.runtimeClasspath
|
||||
into layout.buildDirectory.dir("dependencies")
|
||||
}
|
||||
|
||||
test {
|
||||
|
|
@ -55,10 +57,10 @@ tasks.withType(Test) {
|
|||
TestLogEvent.PASSED,
|
||||
TestLogEvent.SKIPPED,
|
||||
TestLogEvent.STANDARD_OUT
|
||||
exceptionFormat TestExceptionFormat.FULL
|
||||
showExceptions true
|
||||
showCauses true
|
||||
showStackTraces true
|
||||
exceptionFormat = TestExceptionFormat.FULL
|
||||
showExceptions = true
|
||||
showCauses = true
|
||||
showStackTraces = true
|
||||
|
||||
// set options for log level DEBUG and INFO
|
||||
debug {
|
||||
|
|
@ -68,7 +70,7 @@ tasks.withType(Test) {
|
|||
TestLogEvent.SKIPPED,
|
||||
TestLogEvent.STANDARD_ERROR,
|
||||
TestLogEvent.STANDARD_OUT
|
||||
exceptionFormat TestExceptionFormat.FULL
|
||||
exceptionFormat = TestExceptionFormat.FULL
|
||||
}
|
||||
info.events = debug.events
|
||||
info.exceptionFormat = debug.exceptionFormat
|
||||
|
|
|
|||
|
|
@ -1,55 +1,40 @@
|
|||
{
|
||||
"_comment": "Contains list of implementation dependencies URL for this project",
|
||||
"list": [
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1.jar",
|
||||
"name": "Gson 2.3.1",
|
||||
"src": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1.jar",
|
||||
"name": "Jackson Dataformat 2.16.1",
|
||||
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar",
|
||||
"name": "SnakeYAML 2.2",
|
||||
"src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1.jar",
|
||||
"name": "Jackson core 2.16.1",
|
||||
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1.jar",
|
||||
"name": "Jackson databind 2.16.1",
|
||||
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1.jar",
|
||||
"name": "Jackson annotation 2.16.1",
|
||||
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar",
|
||||
"name": "Argon2-jvm 2.11",
|
||||
"src": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm-nolibs/2.11/argon2-jvm-nolibs-2.11.jar",
|
||||
"name": "Argon2-jvm no libs 2.11",
|
||||
"src": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm-nolibs/2.11/argon2-jvm-nolibs-2.11-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar",
|
||||
"name": "SQLite JDBC Driver 3.30.1",
|
||||
"src": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0.jar",
|
||||
"name": "JNA 5.8.0",
|
||||
"src": "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0-sources.jar"
|
||||
}
|
||||
]
|
||||
"_comment": "Contains list of implementation dependencies URL for this project. This is a generated file, don't modify the contents by hand.",
|
||||
"list": [
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.13.1/gson-2.13.1.jar",
|
||||
"name":"gson 2.13.1",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.13.1/gson-2.13.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.38.0/error_prone_annotations-2.38.0.jar",
|
||||
"name":"error_prone_annotations 2.38.0",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.38.0/error_prone_annotations-2.38.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.18.2/jackson-dataformat-yaml-2.18.2.jar",
|
||||
"name":"jackson-dataformat-yaml 2.18.2",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.18.2/jackson-dataformat-yaml-2.18.2-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.3/snakeyaml-2.3.jar",
|
||||
"name":"snakeyaml 2.3",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.3/snakeyaml-2.3-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.18.2/jackson-databind-2.18.2.jar",
|
||||
"name":"jackson-databind 2.18.2",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.18.2/jackson-databind-2.18.2-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar",
|
||||
"name":"argon2-jvm 2.11",
|
||||
"src":"https://repo.maven.apache.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar",
|
||||
"name":"jbcrypt 0.4",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4-sources.jar"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
cli/jar/cli.jar
BIN
cli/jar/cli.jar
Binary file not shown.
|
|
@ -43,12 +43,38 @@ public class StartHandler extends CommandHandler {
|
|||
String host = CLIOptionsParser.parseOption("--host", args);
|
||||
boolean foreground = CLIOptionsParser.hasKey("--foreground", args);
|
||||
boolean forceNoInMemDB = CLIOptionsParser.hasKey("--no-in-mem-db", args);
|
||||
boolean javaagentEnabled = CLIOptionsParser.hasKey("--javaagent", args);
|
||||
boolean jmxEnabled = CLIOptionsParser.hasKey("--jmx", args);
|
||||
String jmxPort = CLIOptionsParser.parseOption("--jmx-port", args);
|
||||
String jmxAuthenticate = CLIOptionsParser.parseOption("--jmx-authenticate", args);
|
||||
String jmxSSL = CLIOptionsParser.parseOption("--jmx-ssl", args);
|
||||
|
||||
List<String> commands = new ArrayList<>();
|
||||
if (OperatingSystem.getOS() == OperatingSystem.OS.WINDOWS) {
|
||||
commands.add(installationDir + "jre\\bin\\java.exe");
|
||||
commands.add("-classpath");
|
||||
commands.add("\"" + installationDir + "core\\*\";\"" + installationDir + "plugin-interface\\*\"");
|
||||
if (javaagentEnabled) {
|
||||
commands.add("-javaagent:\"" + installationDir + "agent\\opentelemetry-javaagent.jar\"");
|
||||
}
|
||||
if (jmxEnabled) {
|
||||
commands.add("-Dcom.sun.management.jmxremote");
|
||||
if (jmxPort != null) {
|
||||
commands.add("-Dcom.sun.management.jmxremote.port=" + jmxPort);
|
||||
} else {
|
||||
commands.add("-Dcom.sun.management.jmxremote.port=9010");
|
||||
}
|
||||
if (jmxAuthenticate != null) {
|
||||
commands.add("-Dcom.sun.management.jmxremote.authenticate=" + jmxAuthenticate);
|
||||
} else {
|
||||
commands.add("-Dcom.sun.management.jmxremote.authenticate=false");
|
||||
}
|
||||
if (jmxSSL != null) {
|
||||
commands.add("-Dcom.sun.management.jmxremote.ssl=" + jmxSSL);
|
||||
} else {
|
||||
commands.add("-Dcom.sun.management.jmxremote.ssl=false");
|
||||
}
|
||||
}
|
||||
if (space != null) {
|
||||
commands.add("-Xmx" + space + "M");
|
||||
}
|
||||
|
|
@ -77,6 +103,27 @@ public class StartHandler extends CommandHandler {
|
|||
commands.add("-classpath");
|
||||
commands.add(
|
||||
installationDir + "core/*:" + installationDir + "plugin-interface/*:" + installationDir + "ee/*");
|
||||
if (javaagentEnabled) {
|
||||
commands.add("-javaagent:" + installationDir + "agent/opentelemetry-javaagent.jar");
|
||||
}
|
||||
if (jmxEnabled) {
|
||||
commands.add("-Dcom.sun.management.jmxremote");
|
||||
if (jmxPort != null) {
|
||||
commands.add("-Dcom.sun.management.jmxremote.port=" + jmxPort);
|
||||
} else {
|
||||
commands.add("-Dcom.sun.management.jmxremote.port=9010");
|
||||
}
|
||||
if (jmxAuthenticate != null) {
|
||||
commands.add("-Dcom.sun.management.jmxremote.authenticate=" + jmxAuthenticate);
|
||||
} else {
|
||||
commands.add("-Dcom.sun.management.jmxremote.authenticate=false");
|
||||
}
|
||||
if (jmxSSL != null) {
|
||||
commands.add("-Dcom.sun.management.jmxremote.ssl=" + jmxSSL);
|
||||
} else {
|
||||
commands.add("-Dcom.sun.management.jmxremote.ssl=false");
|
||||
}
|
||||
}
|
||||
if (space != null) {
|
||||
commands.add("-Xmx" + space + "M");
|
||||
}
|
||||
|
|
@ -101,6 +148,7 @@ public class StartHandler extends CommandHandler {
|
|||
if (!foreground) {
|
||||
try {
|
||||
ProcessBuilder pb = new ProcessBuilder(commands);
|
||||
Logging.info("Command to be run: " + String.join(" ", pb.command()));
|
||||
pb.redirectErrorStream(true);
|
||||
Process process = pb.start();
|
||||
try (InputStreamReader in = new InputStreamReader(process.getInputStream());
|
||||
|
|
@ -181,6 +229,13 @@ public class StartHandler extends CommandHandler {
|
|||
new Option("--foreground", "Runs this instance of SuperTokens in the foreground (not as a daemon)"));
|
||||
options.add(
|
||||
new Option("--with-temp-dir", "Uses the passed dir as temp dir, instead of the internal default."));
|
||||
options.add(new Option("--javaagent", "Enables the OpenTelemetry Javaagent for tracing and metrics."));
|
||||
options.add(new Option("--jmx", "Enables JMX management and monitoring."));
|
||||
options.add(new Option("--jmx-port", "Sets the port for JMX. Defaults to 9010 if --jmx is passed."));
|
||||
options.add(new Option("--jmx-authenticate",
|
||||
"Sets whether JMX authentication is enabled or not. Defaults to false if --jmx is passed."));
|
||||
options.add(new Option("--jmx-ssl",
|
||||
"Sets whether JMX SSL is enabled or not. Defaults to false if --jmx is passed."));
|
||||
return options;
|
||||
}
|
||||
|
||||
|
|
|
|||
27
config.yaml
27
config.yaml
|
|
@ -174,3 +174,30 @@ core_config_version: 0
|
|||
# (DIFFERENT_ACROSS_APPS | OPTIONAL | Default: number of available processor cores) int value. If specified,
|
||||
# the supertokens core will use the specified number of threads to complete the migration of users.
|
||||
# bulk_migration_parallelism:
|
||||
|
||||
# (DIFFERENT_ACROSS_APPS | OPTIONAL | Default: 8000) int value. If specified, the supertokens core will load the
|
||||
# specified number of users for migrating in one single batch.
|
||||
# bulk_migration_batch_size:
|
||||
|
||||
# (DIFFERENT_ACROSS_APPS | OPTIONAL | Default: 3600000) long value. Time in milliseconds for how long a webauthn
|
||||
# account recovery token is valid for.
|
||||
# webauthn_recover_account_token_lifetime:
|
||||
|
||||
# (OPTIONAL | Default: null) string value. The URL of the OpenTelemetry collector to which the core
|
||||
# will send telemetry data. This should be in the format http://<host>:<port> or https://<host>:<port>.
|
||||
# otel_collector_connection_uri:
|
||||
|
||||
# (OPTIONAL | Default: false) boolean value. Enables or disables the deadlock logger.
|
||||
# deadlock_logger_enable:
|
||||
|
||||
# (OPTIONAL | Default: null) string value. If specified, uses this URL as ACS URL for handling legacy SAML clients
|
||||
# saml_legacy_acs_url:
|
||||
|
||||
# (OPTIONAL | Default: https://saml.supertokens.com) string value. Service provider's entity ID.
|
||||
# saml_sp_entity_id:
|
||||
|
||||
# OPTIONAL | Default: 300000) long value. Duration for which SAML claims will be valid before it is consumed
|
||||
# saml_claims_validity:
|
||||
|
||||
# OPTIONAL | Default: 300000) long value. Duration for which SAML relay state will be valid before it is consumed
|
||||
# saml_relay_state_validity:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@
|
|||
"4.0",
|
||||
"5.0",
|
||||
"5.1",
|
||||
"5.2"
|
||||
"5.2",
|
||||
"5.3",
|
||||
"5.4"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,3 +175,29 @@ disable_telemetry: true
|
|||
# the supertokens core will use the specified number of threads to complete the migration of users.
|
||||
# bulk_migration_parallelism:
|
||||
|
||||
# (DIFFERENT_ACROSS_APPS | OPTIONAL | Default: 8000) int value. If specified, the supertokens core will load the
|
||||
# specified number of users for migrating in one single batch.
|
||||
# bulk_migration_batch_size:
|
||||
|
||||
# (DIFFERENT_ACROSS_APPS | OPTIONAL | Default: 3600000) long value. Time in milliseconds for how long a webauthn
|
||||
# account recovery token is valid for.
|
||||
# webauthn_recover_account_token_lifetime:
|
||||
|
||||
# (OPTIONAL | Default: null) string value. The URL of the OpenTelemetry collector to which the core
|
||||
# will send telemetry data. This should be in the format http://<host>:<port> or https://<host>:<port>.
|
||||
# otel_collector_connection_uri:
|
||||
|
||||
# (OPTIONAL | Default: false) boolean value. Enables or disables the deadlock logger.
|
||||
# deadlock_logger_enable:
|
||||
|
||||
# (OPTIONAL | Default: null) string value. If specified, uses this URL as ACS URL for handling legacy SAML clients
|
||||
saml_legacy_acs_url: "http://localhost:5225/api/oauth/saml"
|
||||
|
||||
# (OPTIONAL | Default: https://saml.supertokens.com) string value. Service provider's entity ID.
|
||||
# saml_sp_entity_id:
|
||||
|
||||
# OPTIONAL | Default: 300000) long value. Duration for which SAML claims will be valid before it is consumed
|
||||
# saml_claims_validity:
|
||||
|
||||
# OPTIONAL | Default: 300000) long value. Duration for which SAML relay state will be valid before it is consumed
|
||||
# saml_relay_state_validity:
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ dependencies {
|
|||
testImplementation group: 'junit', name: 'junit', version: '4.12'
|
||||
}
|
||||
|
||||
task copyJars(type: Copy) {
|
||||
into "$buildDir/dependencies"
|
||||
tasks.register('copyJars', Copy) {
|
||||
from configurations.runtimeClasspath
|
||||
into layout.buildDirectory.dir("dependencies")
|
||||
}
|
||||
|
||||
test {
|
||||
|
|
@ -56,10 +56,10 @@ tasks.withType(Test) {
|
|||
TestLogEvent.PASSED,
|
||||
TestLogEvent.SKIPPED,
|
||||
TestLogEvent.STANDARD_OUT
|
||||
exceptionFormat TestExceptionFormat.FULL
|
||||
showExceptions true
|
||||
showCauses true
|
||||
showStackTraces true
|
||||
exceptionFormat = TestExceptionFormat.FULL
|
||||
showExceptions = true
|
||||
showCauses = true
|
||||
showStackTraces = true
|
||||
|
||||
// set options for log level DEBUG and INFO
|
||||
debug {
|
||||
|
|
@ -69,7 +69,7 @@ tasks.withType(Test) {
|
|||
TestLogEvent.SKIPPED,
|
||||
TestLogEvent.STANDARD_ERROR,
|
||||
TestLogEvent.STANDARD_OUT
|
||||
exceptionFormat TestExceptionFormat.FULL
|
||||
exceptionFormat = TestExceptionFormat.FULL
|
||||
}
|
||||
info.events = debug.events
|
||||
info.exceptionFormat = debug.exceptionFormat
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -14,6 +14,7 @@ exitIfNeeded
|
|||
|
||||
exitIfNeeded
|
||||
|
||||
|
||||
(cd ../../ && ./gradlew :$prefix-core:downloader:copyJars < /dev/null)
|
||||
|
||||
exitIfNeeded
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ plugins {
|
|||
id 'java-library'
|
||||
}
|
||||
|
||||
version 'unspecified'
|
||||
version = 'unspecified'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
maven { url 'https://build.shibboleth.net/nexus/content/repositories/releases/' }
|
||||
}
|
||||
|
||||
jar {
|
||||
|
|
@ -13,7 +15,7 @@ jar {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
|
||||
compileOnly group: 'com.google.code.gson', name: 'gson', version: '2.13.1'
|
||||
|
||||
compileOnly project(":supertokens-plugin-interface")
|
||||
testImplementation project(":supertokens-plugin-interface")
|
||||
|
|
@ -35,13 +37,13 @@ dependencies {
|
|||
testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.1.0'
|
||||
|
||||
// https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core
|
||||
testImplementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18'
|
||||
testImplementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '11.0.5'
|
||||
|
||||
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
|
||||
testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14'
|
||||
testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.13'
|
||||
|
||||
// https://mvnrepository.com/artifact/com.google.code.gson/gson
|
||||
testImplementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1'
|
||||
testImplementation group: 'com.google.code.gson', name: 'gson', version: '2.13.1'
|
||||
|
||||
testImplementation 'com.tngtech.archunit:archunit-junit4:0.22.0'
|
||||
|
||||
|
|
@ -52,17 +54,18 @@ dependencies {
|
|||
testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1'
|
||||
|
||||
testImplementation group: 'org.jetbrains', name: 'annotations', version: '13.0'
|
||||
|
||||
}
|
||||
|
||||
task copyJars(type: Copy) {
|
||||
into "$buildDir/dependencies"
|
||||
tasks.register('copyJars', Copy) {
|
||||
from configurations.runtimeClasspath
|
||||
into layout.buildDirectory.dir("dependencies")
|
||||
}
|
||||
|
||||
def interfaceName = "io.supertokens.featureflag.EEFeatureFlagInterface"
|
||||
def className = "io.supertokens.ee.EEFeatureFlag"
|
||||
|
||||
task generateMetaInf {
|
||||
tasks.register('generateMetaInf') {
|
||||
doFirst {
|
||||
mkdir "src/main/resources/META-INF/services"
|
||||
file("src/main/resources/META-INF/services/${interfaceName}").text = "${className}"
|
||||
|
|
@ -89,10 +92,10 @@ tasks.withType(Test) {
|
|||
TestLogEvent.PASSED,
|
||||
TestLogEvent.SKIPPED,
|
||||
TestLogEvent.STANDARD_OUT
|
||||
exceptionFormat TestExceptionFormat.FULL
|
||||
showExceptions true
|
||||
showCauses true
|
||||
showStackTraces true
|
||||
exceptionFormat = TestExceptionFormat.FULL
|
||||
showExceptions = true
|
||||
showCauses = true
|
||||
showStackTraces = true
|
||||
|
||||
// set options for log level DEBUG and INFO
|
||||
debug {
|
||||
|
|
@ -102,7 +105,7 @@ tasks.withType(Test) {
|
|||
TestLogEvent.SKIPPED,
|
||||
TestLogEvent.STANDARD_ERROR,
|
||||
TestLogEvent.STANDARD_OUT
|
||||
exceptionFormat TestExceptionFormat.FULL
|
||||
exceptionFormat = TestExceptionFormat.FULL
|
||||
}
|
||||
info.events = debug.events
|
||||
info.exceptionFormat = debug.exceptionFormat
|
||||
|
|
|
|||
BIN
ee/jar/ee.jar
BIN
ee/jar/ee.jar
Binary file not shown.
|
|
@ -14,6 +14,7 @@ exitIfNeeded
|
|||
|
||||
exitIfNeeded
|
||||
|
||||
|
||||
(cd ../../ && ./gradlew :$prefix-core:ee:copyJars < /dev/null)
|
||||
|
||||
exitIfNeeded
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
|||
import io.supertokens.pluginInterface.multitenancy.ThirdPartyConfig;
|
||||
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
|
||||
import io.supertokens.pluginInterface.oauth.OAuthStorage;
|
||||
import io.supertokens.pluginInterface.saml.SAMLStorage;
|
||||
import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage;
|
||||
import io.supertokens.storageLayer.StorageLayer;
|
||||
import io.supertokens.utils.Utils;
|
||||
|
|
@ -386,6 +387,34 @@ public class EEFeatureFlag implements io.supertokens.featureflag.EEFeatureFlagIn
|
|||
return mauArr;
|
||||
}
|
||||
|
||||
private JsonObject getSAMLStats() throws TenantOrAppNotFoundException, StorageQueryException {
|
||||
JsonObject stats = new JsonObject();
|
||||
|
||||
stats.addProperty("connectionUriDomain", this.appIdentifier.getConnectionUriDomain());
|
||||
stats.addProperty("appId", this.appIdentifier.getAppId());
|
||||
|
||||
JsonArray tenantStats = new JsonArray();
|
||||
|
||||
TenantConfig[] tenantConfigs = Multitenancy.getAllTenantsForApp(this.appIdentifier, main);
|
||||
for (TenantConfig tenantConfig : tenantConfigs) {
|
||||
JsonObject tenantStat = new JsonObject();
|
||||
tenantStat.addProperty("tenantId", tenantConfig.tenantIdentifier.getTenantId());
|
||||
|
||||
{
|
||||
Storage storage = StorageLayer.getStorage(tenantConfig.tenantIdentifier, main);
|
||||
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
|
||||
|
||||
JsonObject stat = new JsonObject();
|
||||
stat.addProperty("numberOfSAMLClients", samlStorage.countSAMLClients(tenantConfig.tenantIdentifier));
|
||||
stat.add(tenantConfig.tenantIdentifier.getTenantId(), stat);
|
||||
}
|
||||
}
|
||||
|
||||
stats.add("tenants", tenantStats);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObject getPaidFeatureStats() throws StorageQueryException, TenantOrAppNotFoundException {
|
||||
JsonObject usageStats = new JsonObject();
|
||||
|
|
@ -433,6 +462,10 @@ public class EEFeatureFlag implements io.supertokens.featureflag.EEFeatureFlagIn
|
|||
if (feature == EE_FEATURES.OAUTH) {
|
||||
usageStats.add(EE_FEATURES.OAUTH.toString(), getOAuthStats());
|
||||
}
|
||||
|
||||
if (feature == EE_FEATURES.SAML) {
|
||||
usageStats.add(EE_FEATURES.SAML.toString(), getSAMLStats());
|
||||
}
|
||||
}
|
||||
|
||||
usageStats.add("maus", getMAUs());
|
||||
|
|
@ -523,7 +556,7 @@ public class EEFeatureFlag implements io.supertokens.featureflag.EEFeatureFlagIn
|
|||
ProcessState.getInstance(main)
|
||||
.addState(ProcessState.PROCESS_STATE.LICENSE_KEY_CHECK_NETWORK_CALL, null, json);
|
||||
JsonObject licenseCheckResponse = HttpRequest.sendJsonPOSTRequest(this.main, REQUEST_ID,
|
||||
"https://api.supertokens.io/0/st/license/check",
|
||||
"https://api.supertokens.com/0/st/license/check",
|
||||
json, 10000, 10000, 0);
|
||||
if (licenseCheckResponse.get("status").getAsString().equalsIgnoreCase("OK")) {
|
||||
Logging.debug(main, appIdentifier.getAsPublicTenantIdentifier(), "API returned OK");
|
||||
|
|
|
|||
|
|
@ -44,14 +44,14 @@ public class TestMultitenancyStats {
|
|||
String[] args = {"../../"};
|
||||
|
||||
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
|
||||
CronTaskTest.getInstance(process.main).setIntervalInSeconds(EELicenseCheck.RESOURCE_KEY, 1);
|
||||
CronTaskTest.getInstance(process.getProcess()).setIntervalInSeconds(EELicenseCheck.RESOURCE_KEY, 1);
|
||||
Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
|
||||
|
||||
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (StorageLayer.isInMemDb(process.main)) {
|
||||
if (StorageLayer.isInMemDb(process.getProcess())) {
|
||||
// cause we keep all features enabled in memdb anyway
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public class TestingProcessManager {
|
|||
String[] args = {"../../"};
|
||||
TestingProcess process = TestingProcessManager.start(args);
|
||||
process.checkOrWaitForEvent(PROCESS_STATE.STARTED);
|
||||
process.main.deleteAllInformationForTesting();
|
||||
process.getProcess().deleteAllInformationForTesting();
|
||||
process.kill();
|
||||
System.out.println("----------DELETE ALL INFORMATION----------");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ public abstract class Utils extends Mockito {
|
|||
try {
|
||||
|
||||
// remove config.yaml file
|
||||
ProcessBuilder pb = new ProcessBuilder("rm", "config.yaml");
|
||||
String workerId = System.getProperty("org.gradle.test.worker", "");
|
||||
ProcessBuilder pb = new ProcessBuilder("rm", "config" + workerId + ".yaml");
|
||||
pb.directory(new File(installDir));
|
||||
Process process = pb.start();
|
||||
process.waitFor();
|
||||
|
|
@ -58,7 +59,8 @@ public abstract class Utils extends Mockito {
|
|||
|
||||
// if the default config is not the same as the current config, we must reset the storage layer
|
||||
File ogConfig = new File("../../temp/config.yaml");
|
||||
File currentConfig = new File("../../config.yaml");
|
||||
String workerId = System.getProperty("org.gradle.test.worker", "");
|
||||
File currentConfig = new File("../../config" + workerId + ".yaml");
|
||||
if (currentConfig.isFile()) {
|
||||
byte[] ogConfigContent = Files.readAllBytes(ogConfig.toPath());
|
||||
byte[] currentConfigContent = Files.readAllBytes(currentConfig.toPath());
|
||||
|
|
@ -67,7 +69,7 @@ public abstract class Utils extends Mockito {
|
|||
}
|
||||
}
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder("cp", "temp/config.yaml", "./config.yaml");
|
||||
ProcessBuilder pb = new ProcessBuilder("cp", "temp/config.yaml", "./config" + workerId + ".yaml");
|
||||
pb.directory(new File(installDir));
|
||||
Process process = pb.start();
|
||||
process.waitFor();
|
||||
|
|
@ -96,14 +98,15 @@ public abstract class Utils extends Mockito {
|
|||
String newStr = "\n# " + key + ":";
|
||||
|
||||
StringBuilder originalFileContent = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader("../../config.yaml"))) {
|
||||
String workerId = System.getProperty("org.gradle.test.worker", "");
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader("../../config" + workerId + ".yaml"))) {
|
||||
String currentReadingLine = reader.readLine();
|
||||
while (currentReadingLine != null) {
|
||||
originalFileContent.append(currentReadingLine).append(System.lineSeparator());
|
||||
currentReadingLine = reader.readLine();
|
||||
}
|
||||
String modifiedFileContent = originalFileContent.toString().replaceAll(oldStr, newStr);
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter("../../config.yaml"))) {
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter("../../config" + workerId + ".yaml"))) {
|
||||
writer.write(modifiedFileContent);
|
||||
}
|
||||
}
|
||||
|
|
@ -117,14 +120,15 @@ public abstract class Utils extends Mockito {
|
|||
String oldStr = "\n((#\\s)?)" + key + "(:|((:\\s).+))\n";
|
||||
String newStr = "\n" + key + ": " + value + "\n";
|
||||
StringBuilder originalFileContent = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader("../../config.yaml"))) {
|
||||
String workerId = System.getProperty("org.gradle.test.worker", "");
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader("../../config" + workerId + ".yaml"))) {
|
||||
String currentReadingLine = reader.readLine();
|
||||
while (currentReadingLine != null) {
|
||||
originalFileContent.append(currentReadingLine).append(System.lineSeparator());
|
||||
currentReadingLine = reader.readLine();
|
||||
}
|
||||
String modifiedFileContent = originalFileContent.toString().replaceAll(oldStr, newStr);
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter("../../config.yaml"))) {
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter("../../config" + workerId + ".yaml"))) {
|
||||
writer.write(modifiedFileContent);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ public class DeleteLicenseKeyAPITest {
|
|||
|
||||
// check that no LicenseKey exits
|
||||
try {
|
||||
FeatureFlag.getInstance(process.main).getLicenseKey();
|
||||
FeatureFlag.getInstance(process.getProcess()).getLicenseKey();
|
||||
fail();
|
||||
} catch (NoLicenseKeyFoundException ignored) {
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ public class DeleteLicenseKeyAPITest {
|
|||
|
||||
// check that no LicenseKey exits
|
||||
try {
|
||||
FeatureFlag.getInstance(process.main).getLicenseKey();
|
||||
FeatureFlag.getInstance(process.getProcess()).getLicenseKey();
|
||||
fail();
|
||||
} catch (NoLicenseKeyFoundException ignored) {
|
||||
}
|
||||
|
|
@ -90,7 +90,7 @@ public class DeleteLicenseKeyAPITest {
|
|||
|
||||
// check that no LicenseKey exits
|
||||
try {
|
||||
FeatureFlag.getInstance(process.main).getLicenseKey();
|
||||
FeatureFlag.getInstance(process.getProcess()).getLicenseKey();
|
||||
fail();
|
||||
} catch (NoLicenseKeyFoundException ignored) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ public class GetFeatureFlagAPITest {
|
|||
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
|
||||
Assert.assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STARTED));
|
||||
|
||||
if (StorageLayer.isInMemDb(process.main)) {
|
||||
if (StorageLayer.isInMemDb(process.getProcess())) {
|
||||
// cause we keep all features enabled in memdb anyway
|
||||
return;
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ public class GetFeatureFlagAPITest {
|
|||
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
|
||||
Assert.assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STARTED));
|
||||
|
||||
if (StorageLayer.isInMemDb(process.main)) {
|
||||
if (StorageLayer.isInMemDb(process.getProcess())) {
|
||||
// cause we keep all features enabled in memdb anyway
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ public class GetLicenseKeyAPITest {
|
|||
|
||||
assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STARTED));
|
||||
|
||||
Assert.assertNull(FeatureFlag.getInstance(process.main).getEeFeatureFlagInstance());
|
||||
Assert.assertNull(FeatureFlag.getInstance(process.getProcess()).getEeFeatureFlagInstance());
|
||||
|
||||
Assert.assertEquals(FeatureFlag.getInstance(process.getProcess()).getEnabledFeatures().length, 0);
|
||||
|
||||
|
|
|
|||
|
|
@ -74,9 +74,9 @@ public class SetLicenseKeyAPITest {
|
|||
|
||||
assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STARTED));
|
||||
|
||||
Assert.assertNull(FeatureFlag.getInstance(process.main).getEeFeatureFlagInstance());
|
||||
Assert.assertNull(FeatureFlag.getInstance(process.getProcess()).getEeFeatureFlagInstance());
|
||||
|
||||
Assert.assertEquals(FeatureFlag.getInstance(process.getProcess()).getEnabledFeatures().length, 0);
|
||||
Assert.assertEquals(0, FeatureFlag.getInstance(process.getProcess()).getEnabledFeatures().length);
|
||||
|
||||
// set license key when ee folder does not exist
|
||||
JsonObject requestBody = new JsonObject();
|
||||
|
|
|
|||
|
|
@ -1,120 +1,285 @@
|
|||
{
|
||||
"_comment": "Contains list of implementation dependencies URL for this project",
|
||||
"list": [
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1.jar",
|
||||
"name": "Gson 2.3.1",
|
||||
"src": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1.jar",
|
||||
"name": "Jackson Dataformat 2.16.1",
|
||||
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar",
|
||||
"name": "SnakeYAML 2.2",
|
||||
"src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1.jar",
|
||||
"name": "Jackson core 2.16.1",
|
||||
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1.jar",
|
||||
"name": "Jackson databind 2.16.1",
|
||||
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1.jar",
|
||||
"name": "Jackson annotation 2.16.1",
|
||||
"src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.4.14/logback-classic-1.4.14.jar",
|
||||
"name": "Logback classic 1.4.14",
|
||||
"src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.4.14/logback-classic-1.4.14-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.4.14/logback-core-1.4.14.jar",
|
||||
"name": "Logback core 1.4.14",
|
||||
"src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.4.14/logback-core-1.4.14-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.7/slf4j-api-2.0.7.jar",
|
||||
"name": "SLF4j API 2.0.7",
|
||||
"src": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.7/slf4j-api-2.0.7-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.18/tomcat-annotations-api-10.1.18.jar",
|
||||
"name": "Tomcat annotations API 10.1.18",
|
||||
"src": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.18/tomcat-annotations-api-10.1.18-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18.jar",
|
||||
"name": "Tomcat embed core API 10.1.1",
|
||||
"src": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar",
|
||||
"name": "JSR305 3.0.2",
|
||||
"src": "https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.jar",
|
||||
"name": "JSR305 3.0.2",
|
||||
"src": "https://repo1.maven.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar",
|
||||
"name": "SQLite JDBC Driver 3.45.1.0",
|
||||
"src": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar",
|
||||
"name": "JBCrypt 0.4",
|
||||
"src": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/auth0/java-jwt/4.4.0/java-jwt-4.4.0.jar",
|
||||
"name": "Auth0 Java JWT",
|
||||
"src": "https://repo1.maven.org/maven2/com/auth0/java-jwt/4.4.0/java-jwt-4.4.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar",
|
||||
"name": "Argon2-jvm 2.11",
|
||||
"src": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm-nolibs/2.11/argon2-jvm-nolibs-2.11.jar",
|
||||
"name": "Argon2-jvm no libs 2.11",
|
||||
"src": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm-nolibs/2.11/argon2-jvm-nolibs-2.11-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0.jar",
|
||||
"name": "JNA 5.8.0",
|
||||
"src": "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.8.0/jna-5.8.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0.jar",
|
||||
"name": "Scrypt 1.4.0",
|
||||
"src": "https://repo1.maven.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/eatthepath/java-otp/0.4.0/java-otp-0.4.0.jar",
|
||||
"name": "Java OTP 0.4.0",
|
||||
"src": "https://repo1.maven.org/maven2/com/eatthepath/java-otp/0.4.0/java-otp-0.4.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15.jar",
|
||||
"name": "Commons Codec 1.15",
|
||||
"src": "https://repo1.maven.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar": "https://repo1.maven.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.25/libphonenumber-8.13.25.jar",
|
||||
"name": "Libphonenumber 8.13.25",
|
||||
"src": "https://repo1.maven.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.25/libphonenumber-8.13.25-sources.jar"
|
||||
}
|
||||
]
|
||||
"_comment": "Contains list of implementation dependencies URL for this project. This is a generated file, don't modify the contents by hand.",
|
||||
"list": [
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/11.0.12/tomcat-embed-core-11.0.12.jar",
|
||||
"name":"tomcat-embed-core 11.0.12",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/11.0.12/tomcat-embed-core-11.0.12-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat-annotations-api/11.0.12/tomcat-annotations-api-11.0.12.jar",
|
||||
"name":"tomcat-annotations-api 11.0.12",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat-annotations-api/11.0.12/tomcat-annotations-api-11.0.12-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.13.1/gson-2.13.1.jar",
|
||||
"name":"gson 2.13.1",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.13.1/gson-2.13.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.38.0/error_prone_annotations-2.38.0.jar",
|
||||
"name":"error_prone_annotations 2.38.0",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/google/errorprone/error_prone_annotations/2.38.0/error_prone_annotations-2.38.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.18.2/jackson-dataformat-yaml-2.18.2.jar",
|
||||
"name":"jackson-dataformat-yaml 2.18.2",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.18.2/jackson-dataformat-yaml-2.18.2-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.3/snakeyaml-2.3.jar",
|
||||
"name":"snakeyaml 2.3",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/yaml/snakeyaml/2.3/snakeyaml-2.3-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.18.2/jackson-dataformat-cbor-2.18.2.jar",
|
||||
"name":"jackson-dataformat-cbor 2.18.2",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.18.2/jackson-dataformat-cbor-2.18.2-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.18.2/jackson-databind-2.18.2.jar",
|
||||
"name":"jackson-databind 2.18.2",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.18.2/jackson-databind-2.18.2-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar",
|
||||
"name":"jsr305 3.0.2",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar",
|
||||
"name":"sqlite-jdbc 3.45.1.0",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar",
|
||||
"name":"slf4j-api 2.0.17",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar",
|
||||
"name":"jbcrypt 0.4",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.jar",
|
||||
"name":"annotations 13.0",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar",
|
||||
"name":"argon2-jvm 2.11",
|
||||
"src":"https://repo.maven.apache.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/auth0/java-jwt/4.4.0/java-jwt-4.4.0.jar",
|
||||
"name":"java-jwt 4.4.0",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/auth0/java-jwt/4.4.0/java-jwt-4.4.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0.jar",
|
||||
"name":"scrypt 1.4.0",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/lambdaworks/scrypt/1.4.0/scrypt-1.4.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/eatthepath/java-otp/0.4.0/java-otp-0.4.0.jar",
|
||||
"name":"java-otp 0.4.0",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/eatthepath/java-otp/0.4.0/java-otp-0.4.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15.jar",
|
||||
"name":"commons-codec 1.15",
|
||||
"src":"https://repo.maven.apache.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.25/libphonenumber-8.13.25.jar",
|
||||
"name":"libphonenumber 8.13.25",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.25/libphonenumber-8.13.25-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/webauthn4j/webauthn4j-core/0.28.6.RELEASE/webauthn4j-core-0.28.6.RELEASE.jar",
|
||||
"name":"webauthn4j-core 0.28.6.RELEASE",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/webauthn4j/webauthn4j-core/0.28.6.RELEASE/webauthn4j-core-0.28.6.RELEASE-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-core/4.3.1/opensaml-core-4.3.1.jar",
|
||||
"name":"opensaml-core 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-core/4.3.1/opensaml-core-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/net/shibboleth/utilities/java-support/8.4.1/java-support-8.4.1.jar",
|
||||
"name":"java-support 8.4.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/net/shibboleth/utilities/java-support/8.4.1/java-support-8.4.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/google/guava/guava/31.1-jre/guava-31.1-jre.jar",
|
||||
"name":"guava 31.1-jre",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/google/guava/guava/31.1-jre/guava-31.1-jre-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar",
|
||||
"name":"failureaccess 1.0.1",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar",
|
||||
"name":"listenablefuture 9999.0-empty-to-avoid-conflict-with-guava",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar",
|
||||
"name":"j2objc-annotations 1.3",
|
||||
"src":"https://repo.maven.apache.org/maven2/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/io/dropwizard/metrics/metrics-core/4.2.25/metrics-core-4.2.25.jar",
|
||||
"name":"metrics-core 4.2.25",
|
||||
"src":"https://repo.maven.apache.org/maven2/io/dropwizard/metrics/metrics-core/4.2.25/metrics-core-4.2.25-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-saml-impl/4.3.1/opensaml-saml-impl-4.3.1.jar",
|
||||
"name":"opensaml-saml-impl 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-saml-impl/4.3.1/opensaml-saml-impl-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-xmlsec-impl/4.3.1/opensaml-xmlsec-impl-4.3.1.jar",
|
||||
"name":"opensaml-xmlsec-impl 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-xmlsec-impl/4.3.1/opensaml-xmlsec-impl-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-security-impl/4.3.1/opensaml-security-impl-4.3.1.jar",
|
||||
"name":"opensaml-security-impl 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-security-impl/4.3.1/opensaml-security-impl-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-security-api/4.3.1/opensaml-security-api-4.3.1.jar",
|
||||
"name":"opensaml-security-api 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-security-api/4.3.1/opensaml-security-api-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-messaging-api/4.3.1/opensaml-messaging-api-4.3.1.jar",
|
||||
"name":"opensaml-messaging-api 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-messaging-api/4.3.1/opensaml-messaging-api-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.jar",
|
||||
"name":"httpclient 4.5.14",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16.jar",
|
||||
"name":"httpcore 4.4.16",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/cryptacular/cryptacular/1.2.5/cryptacular-1.2.5.jar",
|
||||
"name":"cryptacular 1.2.5",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/cryptacular/cryptacular/1.2.5/cryptacular-1.2.5-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk18on/1.72/bcprov-jdk18on-1.72.jar",
|
||||
"name":"bcprov-jdk18on 1.72",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk18on/1.72/bcprov-jdk18on-1.72-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/bouncycastle/bcpkix-jdk18on/1.72/bcpkix-jdk18on-1.72.jar",
|
||||
"name":"bcpkix-jdk18on 1.72",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/bouncycastle/bcpkix-jdk18on/1.72/bcpkix-jdk18on-1.72-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/bouncycastle/bcutil-jdk18on/1.72/bcutil-jdk18on-1.72.jar",
|
||||
"name":"bcutil-jdk18on 1.72",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/bouncycastle/bcutil-jdk18on/1.72/bcutil-jdk18on-1.72-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-xmlsec-api/4.3.1/opensaml-xmlsec-api-4.3.1.jar",
|
||||
"name":"opensaml-xmlsec-api 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-xmlsec-api/4.3.1/opensaml-xmlsec-api-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/apache/santuario/xmlsec/2.3.4/xmlsec-2.3.4.jar",
|
||||
"name":"xmlsec 2.3.4",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/apache/santuario/xmlsec/2.3.4/xmlsec-2.3.4-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-saml-api/4.3.1/opensaml-saml-api-4.3.1.jar",
|
||||
"name":"opensaml-saml-api 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-saml-api/4.3.1/opensaml-saml-api-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-profile-api/4.3.1/opensaml-profile-api-4.3.1.jar",
|
||||
"name":"opensaml-profile-api 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-profile-api/4.3.1/opensaml-profile-api-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-soap-api/4.3.1/opensaml-soap-api-4.3.1.jar",
|
||||
"name":"opensaml-soap-api 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-soap-api/4.3.1/opensaml-soap-api-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-soap-impl/4.3.1/opensaml-soap-impl-4.3.1.jar",
|
||||
"name":"opensaml-soap-impl 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-soap-impl/4.3.1/opensaml-soap-impl-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-storage-api/4.3.1/opensaml-storage-api-4.3.1.jar",
|
||||
"name":"opensaml-storage-api 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-storage-api/4.3.1/opensaml-storage-api-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/apache/velocity/velocity-engine-core/2.3/velocity-engine-core-2.3.jar",
|
||||
"name":"velocity-engine-core 2.3",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/apache/velocity/velocity-engine-core/2.3/velocity-engine-core-2.3-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.11/commons-lang3-3.11.jar",
|
||||
"name":"commons-lang3 3.11",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.11/commons-lang3-3.11-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-profile-impl/4.3.1/opensaml-profile-impl-4.3.1.jar",
|
||||
"name":"opensaml-profile-impl 4.3.1",
|
||||
"src":"https://build.shibboleth.net/nexus/content/repositories/releases/org/opensaml/opensaml-profile-impl/4.3.1/opensaml-profile-impl-4.3.1-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/ch/qos/logback/logback-core/1.5.18/logback-core-1.5.18.jar",
|
||||
"name":"logback-core 1.5.18",
|
||||
"src":"https://repo.maven.apache.org/maven2/ch/qos/logback/logback-core/1.5.18/logback-core-1.5.18-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/ch/qos/logback/logback-classic/1.5.18/logback-classic-1.5.18.jar",
|
||||
"name":"logback-classic 1.5.18",
|
||||
"src":"https://repo.maven.apache.org/maven2/ch/qos/logback/logback-classic/1.5.18/logback-classic-1.5.18-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/org/aspectj/aspectjrt/1.9.24/aspectjrt-1.9.24.jar",
|
||||
"name":"aspectjrt 1.9.24",
|
||||
"src":"https://repo.maven.apache.org/maven2/org/aspectj/aspectjrt/1.9.24/aspectjrt-1.9.24-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-api/1.51.0/opentelemetry-api-1.51.0.jar",
|
||||
"name":"opentelemetry-api 1.51.0",
|
||||
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-api/1.51.0/opentelemetry-api-1.51.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-exporter-logging/1.51.0/opentelemetry-exporter-logging-1.51.0.jar",
|
||||
"name":"opentelemetry-exporter-logging 1.51.0",
|
||||
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-exporter-logging/1.51.0/opentelemetry-exporter-logging-1.51.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-sdk/1.51.0/opentelemetry-sdk-1.51.0.jar",
|
||||
"name":"opentelemetry-sdk 1.51.0",
|
||||
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-sdk/1.51.0/opentelemetry-sdk-1.51.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-exporter-otlp/1.51.0/opentelemetry-exporter-otlp-1.51.0.jar",
|
||||
"name":"opentelemetry-exporter-otlp 1.51.0",
|
||||
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/opentelemetry-exporter-otlp/1.51.0/opentelemetry-exporter-otlp-1.51.0-sources.jar"
|
||||
},
|
||||
{
|
||||
"jar":"https://repo.maven.apache.org/maven2/io/opentelemetry/semconv/opentelemetry-semconv/1.34.0/opentelemetry-semconv-1.34.0.jar",
|
||||
"name":"opentelemetry-semconv 1.34.0",
|
||||
"src":"https://repo.maven.apache.org/maven2/io/opentelemetry/semconv/opentelemetry-semconv/1.34.0/opentelemetry-semconv-1.34.0-sources.jar"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"_comment": "contains a list of plugin interfaces branch names that this core supports",
|
||||
"versions": [
|
||||
"7.0"
|
||||
"8.3"
|
||||
]
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ public class ActiveUsers {
|
|||
@TestOnly
|
||||
public static void updateLastActive(Main main, String userId) {
|
||||
try {
|
||||
ActiveUsers.updateLastActive(new AppIdentifier(null, null),
|
||||
ActiveUsers.updateLastActive(ResourceDistributor.getAppForTesting().toAppIdentifier(),
|
||||
main, userId);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -55,6 +55,6 @@ public class ActiveUsers {
|
|||
@TestOnly
|
||||
public static int countUsersActiveSince(Main main, long time)
|
||||
throws StorageQueryException, TenantOrAppNotFoundException {
|
||||
return countUsersActiveSince(main, new AppIdentifier(null, null), time);
|
||||
return countUsersActiveSince(main, ResourceDistributor.getAppForTesting().toAppIdentifier(), time);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ import io.supertokens.config.CoreConfig;
|
|||
import io.supertokens.cronjobs.Cronjobs;
|
||||
import io.supertokens.cronjobs.bulkimport.ProcessBulkImportUsers;
|
||||
import io.supertokens.cronjobs.cleanupOAuthSessionsAndChallenges.CleanupOAuthSessionsAndChallenges;
|
||||
import io.supertokens.cronjobs.deleteExpiredSAMLData.DeleteExpiredSAMLData;
|
||||
import io.supertokens.cronjobs.cleanupWebauthnExpiredData.CleanUpWebauthNExpiredDataCron;
|
||||
import io.supertokens.cronjobs.deadlocklogger.DeadlockLogger;
|
||||
import io.supertokens.cronjobs.deleteExpiredAccessTokenSigningKeys.DeleteExpiredAccessTokenSigningKeys;
|
||||
import io.supertokens.cronjobs.deleteExpiredDashboardSessions.DeleteExpiredDashboardSessions;
|
||||
import io.supertokens.cronjobs.deleteExpiredEmailVerificationTokens.DeleteExpiredEmailVerificationTokens;
|
||||
|
|
@ -41,7 +44,9 @@ import io.supertokens.pluginInterface.exceptions.DbInitException;
|
|||
import io.supertokens.pluginInterface.exceptions.InvalidConfigException;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
|
||||
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
||||
import io.supertokens.saml.SAMLBootstrap;
|
||||
import io.supertokens.storageLayer.StorageLayer;
|
||||
import io.supertokens.telemetry.TelemetryProvider;
|
||||
import io.supertokens.version.Version;
|
||||
import io.supertokens.webserver.Webserver;
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
|
|
@ -90,6 +95,9 @@ public class Main {
|
|||
private boolean waitToEnableFeatureFlag = false;
|
||||
private final Object waitToEnableFeatureFlagLock = new Object();
|
||||
|
||||
//setting to true by default
|
||||
private final Boolean bulkMigrationCronEnabled = System.getenv("BULK_MIGRATION_CRON_ENABLED") == null || Boolean.parseBoolean(System.getenv("BULK_MIGRATION_CRON_ENABLED"));
|
||||
|
||||
|
||||
private boolean forceInMemoryDB = false;
|
||||
|
||||
|
|
@ -116,6 +124,8 @@ public class Main {
|
|||
CLIOptions.load(this, args);
|
||||
init();
|
||||
} catch (Exception e) {
|
||||
Logging.error(this, TenantIdentifier.BASE_TENANT, "What caused the crash: " + e.getMessage(), true,
|
||||
e);
|
||||
ProcessState.getInstance(this).addState(ProcessState.PROCESS_STATE.INIT_FAILURE, e);
|
||||
throw e;
|
||||
}
|
||||
|
|
@ -150,9 +160,12 @@ public class Main {
|
|||
// Handle kill signal gracefully
|
||||
handleKillSignalForWhenItHappens();
|
||||
|
||||
StorageLayer.loadStorageUCL(CLIOptions.get(this).getInstallationPath() + "plugin/");
|
||||
|
||||
// loading configs for core from config.yaml file.
|
||||
try {
|
||||
Config.loadBaseConfig(this);
|
||||
Logging.info(this, TenantIdentifier.BASE_TENANT, "Completed config.yaml loading.", true);
|
||||
} catch (InvalidConfigException e) {
|
||||
throw new QuitProgramException(e);
|
||||
}
|
||||
|
|
@ -160,12 +173,11 @@ public class Main {
|
|||
// loading version file
|
||||
Version.loadVersion(this, CLIOptions.get(this).getInstallationPath() + "version.yaml");
|
||||
|
||||
Logging.info(this, TenantIdentifier.BASE_TENANT, "Completed config.yaml loading.", true);
|
||||
TelemetryProvider.initialize(this);
|
||||
|
||||
// loading storage layer
|
||||
try {
|
||||
StorageLayer.initPrimary(this, CLIOptions.get(this).getInstallationPath() + "plugin/",
|
||||
Config.getBaseConfigAsJsonObject(this));
|
||||
StorageLayer.initPrimary(this, Config.getBaseConfigAsJsonObject(this));
|
||||
} catch (InvalidConfigException e) {
|
||||
throw new QuitProgramException(e);
|
||||
}
|
||||
|
|
@ -173,6 +185,9 @@ public class Main {
|
|||
// init file logging
|
||||
Logging.initFileLogging(this);
|
||||
|
||||
// Required for SAML related stuff
|
||||
SAMLBootstrap.initialize();
|
||||
|
||||
// initialise cron job handler
|
||||
Cronjobs.init(this);
|
||||
|
||||
|
|
@ -261,10 +276,21 @@ public class Main {
|
|||
Cronjobs.addCronjob(this, DeleteExpiredAccessTokenSigningKeys.init(this, uniqueUserPoolIdsTenants));
|
||||
|
||||
// initializes ProcessBulkImportUsers cronjob to process bulk import users
|
||||
Cronjobs.addCronjob(this, ProcessBulkImportUsers.init(this, uniqueUserPoolIdsTenants));
|
||||
if(bulkMigrationCronEnabled) {
|
||||
Cronjobs.addCronjob(this, ProcessBulkImportUsers.init(this, uniqueUserPoolIdsTenants));
|
||||
}
|
||||
|
||||
Cronjobs.addCronjob(this, CleanupOAuthSessionsAndChallenges.init(this, uniqueUserPoolIdsTenants));
|
||||
|
||||
Cronjobs.addCronjob(this, CleanUpWebauthNExpiredDataCron.init(this, uniqueUserPoolIdsTenants));
|
||||
|
||||
// starts the DeadlockLogger if
|
||||
if (Config.getBaseConfig(this).isDeadlockLoggerEnabled()) {
|
||||
DeadlockLogger.getInstance().start();
|
||||
}
|
||||
|
||||
Cronjobs.addCronjob(this, DeleteExpiredSAMLData.init(this, uniqueUserPoolIdsTenants));
|
||||
|
||||
// this is to ensure tenantInfos are in sync for the new cron job as well
|
||||
MultitenancyHelper.getInstance(this).refreshCronjobs();
|
||||
|
||||
|
|
@ -354,12 +380,16 @@ public class Main {
|
|||
}
|
||||
|
||||
private void createDotStartedFileForThisProcess() throws IOException {
|
||||
String startedDir = ".started";
|
||||
if (isTesting) {
|
||||
startedDir = ".started" + System.getProperty("org.gradle.test.worker", "");
|
||||
}
|
||||
CoreConfig config = Config.getBaseConfig(this);
|
||||
String fileLocation = CLIOptions.get(this).getTempDirLocation() == null ? CLIOptions.get(this).getInstallationPath() : CLIOptions.get(this).getTempDirLocation();
|
||||
String fileName = OperatingSystem.getOS() == OperatingSystem.OS.WINDOWS
|
||||
? fileLocation + ".started\\" + config.getHost(this) + "-"
|
||||
? fileLocation + startedDir + "\\" + config.getHost(this) + "-"
|
||||
+ config.getPort(this)
|
||||
: fileLocation + ".started/" + config.getHost(this) + "-"
|
||||
: fileLocation + startedDir + "/" + config.getHost(this) + "-"
|
||||
+ config.getPort(this);
|
||||
File dotStarted = new File(fileName);
|
||||
if (!dotStarted.exists()) {
|
||||
|
|
@ -402,9 +432,10 @@ public class Main {
|
|||
|
||||
@TestOnly
|
||||
public void killForTestingAndWaitForShutdown() throws InterruptedException {
|
||||
assertIsTesting();
|
||||
wakeUpMainThreadToShutdown();
|
||||
mainThread.join();
|
||||
// Do not kill for now
|
||||
assertIsTesting();
|
||||
wakeUpMainThreadToShutdown();
|
||||
mainThread.join();
|
||||
}
|
||||
|
||||
// must not throw any error
|
||||
|
|
@ -433,6 +464,7 @@ public class Main {
|
|||
StorageLayer.close(this);
|
||||
removeDotStartedFileForThisProcess();
|
||||
Logging.stopLogging(this);
|
||||
TelemetryProvider.closeTelemetry(this);
|
||||
// uncomment this when you want to confirm that processes are actually shut.
|
||||
// printRunningThreadNames();
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ public class ProcessState extends ResourceDistributor.SingletonResource {
|
|||
public static class EventAndException {
|
||||
public Exception exception;
|
||||
public JsonObject data;
|
||||
PROCESS_STATE state;
|
||||
public PROCESS_STATE state;
|
||||
|
||||
public EventAndException(PROCESS_STATE state, Exception e) {
|
||||
this.state = state;
|
||||
|
|
|
|||
|
|
@ -35,16 +35,28 @@ public class ResourceDistributor {
|
|||
private final Map<KeyClass, SingletonResource> resources = new HashMap<>(1);
|
||||
private final Main main;
|
||||
|
||||
private static TenantIdentifier appUsedForTesting = TenantIdentifier.BASE_TENANT;
|
||||
|
||||
public ResourceDistributor(Main main) {
|
||||
this.main = main;
|
||||
}
|
||||
|
||||
public synchronized SingletonResource getResource(AppIdentifier appIdentifier, @Nonnull String key)
|
||||
@TestOnly
|
||||
public static void setAppForTesting(TenantIdentifier app) {
|
||||
appUsedForTesting = app;
|
||||
}
|
||||
|
||||
@TestOnly
|
||||
public static TenantIdentifier getAppForTesting() {
|
||||
return appUsedForTesting;
|
||||
}
|
||||
|
||||
public SingletonResource getResource(AppIdentifier appIdentifier, @Nonnull String key)
|
||||
throws TenantOrAppNotFoundException {
|
||||
return getResource(appIdentifier.getAsPublicTenantIdentifier(), key);
|
||||
}
|
||||
|
||||
public synchronized SingletonResource getResource(TenantIdentifier tenantIdentifier, @Nonnull String key)
|
||||
public SingletonResource getResource(TenantIdentifier tenantIdentifier, @Nonnull String key)
|
||||
throws TenantOrAppNotFoundException {
|
||||
// first we do exact match
|
||||
SingletonResource resource = resources.get(new KeyClass(tenantIdentifier, key));
|
||||
|
|
@ -58,14 +70,6 @@ public class ResourceDistributor {
|
|||
throw new TenantOrAppNotFoundException(tenantIdentifier);
|
||||
}
|
||||
|
||||
MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true);
|
||||
|
||||
// we try again..
|
||||
resource = resources.get(new KeyClass(tenantIdentifier, key));
|
||||
if (resource != null) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
// then we see if the user has configured anything to do with connectionUriDomain, and if they have,
|
||||
// then we must return null cause the user has not specifically added tenantId to it
|
||||
for (KeyClass currKey : resources.keySet()) {
|
||||
|
|
@ -89,11 +93,11 @@ public class ResourceDistributor {
|
|||
}
|
||||
|
||||
@TestOnly
|
||||
public synchronized SingletonResource getResource(@Nonnull String key) {
|
||||
return resources.get(new KeyClass(new TenantIdentifier(null, null, null), key));
|
||||
public SingletonResource getResource(@Nonnull String key) {
|
||||
return resources.get(new KeyClass(appUsedForTesting, key));
|
||||
}
|
||||
|
||||
public synchronized SingletonResource setResource(TenantIdentifier tenantIdentifier,
|
||||
public SingletonResource setResource(TenantIdentifier tenantIdentifier,
|
||||
@Nonnull String key,
|
||||
SingletonResource resource) {
|
||||
SingletonResource alreadyExists = resources.get(new KeyClass(tenantIdentifier, key));
|
||||
|
|
@ -104,7 +108,7 @@ public class ResourceDistributor {
|
|||
return resource;
|
||||
}
|
||||
|
||||
public synchronized SingletonResource removeResource(TenantIdentifier tenantIdentifier,
|
||||
public SingletonResource removeResource(TenantIdentifier tenantIdentifier,
|
||||
@Nonnull String key) {
|
||||
SingletonResource singletonResource = resources.get(new KeyClass(tenantIdentifier, key));
|
||||
if (singletonResource == null) {
|
||||
|
|
@ -114,18 +118,18 @@ public class ResourceDistributor {
|
|||
return singletonResource;
|
||||
}
|
||||
|
||||
public synchronized SingletonResource setResource(AppIdentifier appIdentifier,
|
||||
public SingletonResource setResource(AppIdentifier appIdentifier,
|
||||
@Nonnull String key,
|
||||
SingletonResource resource) {
|
||||
return setResource(appIdentifier.getAsPublicTenantIdentifier(), key, resource);
|
||||
}
|
||||
|
||||
public synchronized SingletonResource removeResource(AppIdentifier appIdentifier,
|
||||
public SingletonResource removeResource(AppIdentifier appIdentifier,
|
||||
@Nonnull String key) {
|
||||
return removeResource(appIdentifier.getAsPublicTenantIdentifier(), key);
|
||||
}
|
||||
|
||||
public synchronized void clearAllResourcesWithResourceKey(String inputKey) {
|
||||
public void clearAllResourcesWithResourceKey(String inputKey) {
|
||||
List<KeyClass> toRemove = new ArrayList<>();
|
||||
resources.forEach((key, value) -> {
|
||||
if (key.key.equals(inputKey)) {
|
||||
|
|
@ -137,7 +141,7 @@ public class ResourceDistributor {
|
|||
}
|
||||
}
|
||||
|
||||
public synchronized Map<KeyClass, SingletonResource> getAllResourcesWithResourceKey(String inputKey) {
|
||||
public Map<KeyClass, SingletonResource> getAllResourcesWithResourceKey(String inputKey) {
|
||||
Map<KeyClass, SingletonResource> result = new HashMap<>();
|
||||
resources.forEach((key, value) -> {
|
||||
if (key.key.equals(inputKey)) {
|
||||
|
|
@ -148,9 +152,9 @@ public class ResourceDistributor {
|
|||
}
|
||||
|
||||
@TestOnly
|
||||
public synchronized SingletonResource setResource(@Nonnull String key,
|
||||
public SingletonResource setResource(@Nonnull String key,
|
||||
SingletonResource resource) {
|
||||
return setResource(new TenantIdentifier(null, null, null), key, resource);
|
||||
return setResource(appUsedForTesting, key, resource);
|
||||
}
|
||||
|
||||
public interface Func<T> {
|
||||
|
|
|
|||
|
|
@ -17,10 +17,9 @@
|
|||
package io.supertokens.authRecipe;
|
||||
|
||||
import io.supertokens.Main;
|
||||
import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException;
|
||||
import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException;
|
||||
import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException;
|
||||
import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithPrimaryUserIdException;
|
||||
import io.supertokens.ResourceDistributor;
|
||||
import io.supertokens.authRecipe.exception.*;
|
||||
import io.supertokens.bulkimport.BulkImportUserUtils;
|
||||
import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
|
||||
import io.supertokens.multitenancy.exception.BadPermissionException;
|
||||
import io.supertokens.pluginInterface.RECIPE_ID;
|
||||
|
|
@ -29,6 +28,7 @@ import io.supertokens.pluginInterface.StorageUtils;
|
|||
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
|
||||
import io.supertokens.pluginInterface.authRecipe.LoginMethod;
|
||||
import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage;
|
||||
import io.supertokens.pluginInterface.bulkimport.BulkImportUser;
|
||||
import io.supertokens.pluginInterface.bulkimport.exceptions.BulkImportBatchInsertException;
|
||||
import io.supertokens.pluginInterface.dashboard.DashboardSearchTags;
|
||||
import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException;
|
||||
|
|
@ -44,6 +44,7 @@ import io.supertokens.session.Session;
|
|||
import io.supertokens.storageLayer.StorageLayer;
|
||||
import io.supertokens.useridmapping.UserIdType;
|
||||
import io.supertokens.utils.Utils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
|
@ -59,7 +60,7 @@ public class AuthRecipe {
|
|||
@TestOnly
|
||||
public static boolean unlinkAccounts(Main main, String recipeUserId)
|
||||
throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException {
|
||||
return unlinkAccounts(main, new AppIdentifier(null, null), StorageLayer.getStorage(main), recipeUserId);
|
||||
return unlinkAccounts(main, ResourceDistributor.getAppForTesting().toAppIdentifier(), StorageLayer.getStorage(main), recipeUserId);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -124,7 +125,7 @@ public class AuthRecipe {
|
|||
@TestOnly
|
||||
public static AuthRecipeUserInfo getUserById(Main main, String userId)
|
||||
throws StorageQueryException {
|
||||
return getUserById(new AppIdentifier(null, null), StorageLayer.getStorage(main), userId);
|
||||
return getUserById(ResourceDistributor.getAppForTesting().toAppIdentifier(), StorageLayer.getStorage(main), userId);
|
||||
}
|
||||
|
||||
public static AuthRecipeUserInfo getUserById(AppIdentifier appIdentifier, Storage storage, String userId)
|
||||
|
|
@ -155,12 +156,15 @@ public class AuthRecipe {
|
|||
}
|
||||
|
||||
public static class CreatePrimaryUserBulkResult {
|
||||
public AuthRecipeUserInfo user;
|
||||
public BulkImportUser user;
|
||||
public BulkImportUser.LoginMethod primaryLoginMethod;
|
||||
public boolean wasAlreadyAPrimaryUser;
|
||||
public Exception error;
|
||||
|
||||
public CreatePrimaryUserBulkResult(AuthRecipeUserInfo user, boolean wasAlreadyAPrimaryUser, Exception error) {
|
||||
public CreatePrimaryUserBulkResult(BulkImportUser user, BulkImportUser.LoginMethod primaryLoginMethod,
|
||||
boolean wasAlreadyAPrimaryUser, Exception error) {
|
||||
this.user = user;
|
||||
this.primaryLoginMethod = primaryLoginMethod;
|
||||
this.wasAlreadyAPrimaryUser = wasAlreadyAPrimaryUser;
|
||||
this.error = error;
|
||||
}
|
||||
|
|
@ -182,16 +186,16 @@ public class AuthRecipe {
|
|||
public String recipeUserId;
|
||||
public String primaryUserId;
|
||||
public Exception error;
|
||||
public AuthRecipeUserInfo authRecipeUserInfo;
|
||||
public BulkImportUser bulkImportUser;
|
||||
public boolean alreadyLinked;
|
||||
|
||||
public CanLinkAccountsBulkResult(String recipeUserId, String primaryUserId, boolean alreadyLinked, Exception error,
|
||||
AuthRecipeUserInfo authRecipeUserInfo) {
|
||||
BulkImportUser bulkImportUser) {
|
||||
this.recipeUserId = recipeUserId;
|
||||
this.primaryUserId = primaryUserId;
|
||||
this.alreadyLinked = alreadyLinked;
|
||||
this.error = error;
|
||||
this.authRecipeUserInfo = authRecipeUserInfo;
|
||||
this.bulkImportUser = bulkImportUser;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +204,7 @@ public class AuthRecipe {
|
|||
throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException,
|
||||
RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException,
|
||||
AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException {
|
||||
return canLinkAccounts(new AppIdentifier(null, null), StorageLayer.getStorage(main), recipeUserId,
|
||||
return canLinkAccounts(ResourceDistributor.getAppForTesting().toAppIdentifier(), StorageLayer.getStorage(main), recipeUserId,
|
||||
primaryUserId);
|
||||
}
|
||||
|
||||
|
|
@ -291,67 +295,55 @@ public class AuthRecipe {
|
|||
return new CanLinkAccountsResult(recipeUser.getSupertokensUserId(), primaryUser.getSupertokensUserId(), false);
|
||||
}
|
||||
|
||||
private static List<CanLinkAccountsBulkResult> canLinkMultipleAccountsHelper(TransactionConnection con,
|
||||
AppIdentifier appIdentifier,
|
||||
Storage storage,
|
||||
Map<String, String> recipeUserIdByPrimaryUserId,
|
||||
List<String> allDistinctEmailAddresses,
|
||||
List<String> phones,
|
||||
Map<String, String> thirdpartyUserIdToId)
|
||||
private static List<CanLinkAccountsBulkResult> canLinkMultipleAccountsHelperForBulkImport(TransactionConnection con,
|
||||
AppIdentifier appIdentifier,
|
||||
Storage storage,
|
||||
List<BulkImportUser> users,
|
||||
List<AuthRecipeUserInfo> allUsersWithExtraData)
|
||||
throws StorageQueryException {
|
||||
AuthRecipeSQLStorage authRecipeStorage = StorageUtils.getAuthRecipeStorage(storage);
|
||||
|
||||
List<CanLinkAccountsBulkResult> results = new ArrayList<>();
|
||||
|
||||
List<AuthRecipeUserInfo> primaryUsers = authRecipeStorage.getPrimaryUsersByIds_Transaction(appIdentifier, con,
|
||||
new ArrayList<>(recipeUserIdByPrimaryUserId.values()));
|
||||
Map<String, String> recipeUserIdByPrimaryUserId = BulkImportUserUtils.collectRecipeIdsToPrimaryIds(users);
|
||||
|
||||
List<AuthRecipeUserInfo> recipeUsers = authRecipeStorage.getPrimaryUsersByIds_Transaction(appIdentifier, con,
|
||||
new ArrayList<>(recipeUserIdByPrimaryUserId.keySet()));
|
||||
|
||||
List<AuthRecipeUserInfo> allUsersWithExtraData =
|
||||
List.of(authRecipeStorage.listPrimaryUsersByMultipleEmailsOrPhoneNumbersOrThirdparty_Transaction
|
||||
(appIdentifier, con, allDistinctEmailAddresses, phones, thirdpartyUserIdToId));
|
||||
|
||||
if(recipeUsers != null && primaryUsers != null) {
|
||||
//collect all the really primary users into a map of userid -> authRecipeUserInfo
|
||||
Map<String, AuthRecipeUserInfo> foundValidPrimaryUsers = primaryUsers.stream().filter(authRecipeUserInfo -> authRecipeUserInfo.isPrimaryUser).collect(Collectors.toMap(AuthRecipeUserInfo::getSupertokensUserId, authRecipeUserInfo -> authRecipeUserInfo));
|
||||
Map<String, AuthRecipeUserInfo> foundRecipeUsers = recipeUsers.stream().collect(Collectors.toMap(AuthRecipeUserInfo::getSupertokensUserId, authRecipeUserInfo -> authRecipeUserInfo));
|
||||
if(recipeUserIdByPrimaryUserId != null && !recipeUserIdByPrimaryUserId.isEmpty()) {
|
||||
|
||||
for(Map.Entry<String, String> recipeUserByPrimaryUser : recipeUserIdByPrimaryUserId.entrySet()) {
|
||||
String recipeUserId = recipeUserByPrimaryUser.getKey();
|
||||
String primaryUserId = recipeUserByPrimaryUser.getValue();
|
||||
AuthRecipeUserInfo primaryUser = foundValidPrimaryUsers.get(primaryUserId);
|
||||
AuthRecipeUserInfo recipeUser = foundRecipeUsers.get(recipeUserId);
|
||||
BulkImportUser.LoginMethod primaryUser = BulkImportUserUtils.findLoginMethodByRecipeUserId(users, primaryUserId);
|
||||
BulkImportUser.LoginMethod recipeUser = BulkImportUserUtils.findLoginMethodByRecipeUserId(users, recipeUserId);
|
||||
if(primaryUser == null || recipeUser == null) {
|
||||
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false, new UnknownUserIdException(), null));
|
||||
} else if(recipeUser.isPrimaryUser) {
|
||||
if (recipeUser.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) {
|
||||
} else if(recipeUser.isPrimary) {
|
||||
if (recipeUser.superTokensUserId.equals(primaryUser.superTokensUserId)) {
|
||||
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, true, null, null));
|
||||
} else {
|
||||
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false, new RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(recipeUser, "The input recipe user ID is already linked to another user ID"), null));
|
||||
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false,
|
||||
new BulkImportRecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(recipeUserId), null));
|
||||
}
|
||||
} else {
|
||||
if (recipeUser.loginMethods.length == 1) {
|
||||
Set<String> tenantIds = new HashSet<>();
|
||||
tenantIds.addAll(recipeUser.tenantIds);
|
||||
tenantIds.addAll(primaryUser.tenantIds);
|
||||
|
||||
try {
|
||||
Set<String> tenantIds = new HashSet<>();
|
||||
tenantIds.addAll(recipeUser.tenantIds);
|
||||
tenantIds.addAll(primaryUser.tenantIds);
|
||||
|
||||
try {
|
||||
bulkCheckIfLoginMethodCanBeLinkedOnTenant(con, appIdentifier, authRecipeStorage, tenantIds,
|
||||
recipeUser, primaryUserId, allUsersWithExtraData);
|
||||
BulkImportUser currentPrimaryUser = BulkImportUserUtils.findUserByPrimaryId(users, primaryUserId);
|
||||
for (BulkImportUser.LoginMethod currLoginMethod : currentPrimaryUser.loginMethods) {
|
||||
bulkCheckIfLoginMethodCanBeLinkedOnTenant(con, appIdentifier, authRecipeStorage, tenantIds,
|
||||
recipeUser.loginMethods[0], primaryUser, allUsersWithExtraData);
|
||||
|
||||
for (LoginMethod currLoginMethod : primaryUser.loginMethods) {
|
||||
bulkCheckIfLoginMethodCanBeLinkedOnTenant(con, appIdentifier, authRecipeStorage, tenantIds,
|
||||
currLoginMethod, primaryUser, allUsersWithExtraData);
|
||||
}
|
||||
|
||||
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false, null, primaryUser));
|
||||
|
||||
} catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException exception) {
|
||||
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false, exception, null));
|
||||
currLoginMethod, primaryUserId, allUsersWithExtraData);
|
||||
}
|
||||
|
||||
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false, null, currentPrimaryUser));
|
||||
|
||||
} catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException exception) {
|
||||
results.add(new CanLinkAccountsBulkResult(recipeUserId, primaryUserId, false, exception, null));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -433,8 +425,8 @@ public class AuthRecipe {
|
|||
|
||||
private static void bulkCheckIfLoginMethodCanBeLinkedOnTenant(TransactionConnection con, AppIdentifier appIdentifier,
|
||||
AuthRecipeSQLStorage authRecipeStorage,
|
||||
Set<String> tenantIds, LoginMethod currLoginMethod,
|
||||
AuthRecipeUserInfo primaryUser,
|
||||
Set<String> tenantIds, BulkImportUser.LoginMethod currLoginMethod,
|
||||
String primaryUserId,
|
||||
List<AuthRecipeUserInfo> allUsersWithExtraData)
|
||||
throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException {
|
||||
// we loop through the union of both the user's tenantIds and check that the criteria for
|
||||
|
|
@ -461,7 +453,7 @@ public class AuthRecipe {
|
|||
if (!user.tenantIds.contains(tenantId)) {
|
||||
continue;
|
||||
}
|
||||
if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) {
|
||||
if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUserId)) {
|
||||
throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
|
||||
user.getSupertokensUserId(),
|
||||
"This user's email is already associated with another user ID");
|
||||
|
|
@ -478,7 +470,7 @@ public class AuthRecipe {
|
|||
if (!user.tenantIds.contains(tenantId)) {
|
||||
continue;
|
||||
}
|
||||
if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) {
|
||||
if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUserId)) {
|
||||
throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
|
||||
user.getSupertokensUserId(),
|
||||
"This user's phone number is already associated with another user" +
|
||||
|
|
@ -487,16 +479,16 @@ public class AuthRecipe {
|
|||
}
|
||||
}
|
||||
|
||||
if (currLoginMethod.thirdParty != null) {
|
||||
if (currLoginMethod.thirdPartyId != null) {
|
||||
List<AuthRecipeUserInfo> extraUsersWithThirdParty = allUsersWithExtraData.stream().filter(authRecipeUserInfo -> Arrays.stream(
|
||||
authRecipeUserInfo.loginMethods).anyMatch(loginMethod1 -> loginMethod1.thirdParty != null)).collect(Collectors.toList());
|
||||
for(AuthRecipeUserInfo extraUser : extraUsersWithThirdParty) {
|
||||
if(extraUser.isPrimaryUser && extraUser.tenantIds.contains(tenantId)
|
||||
&& !extraUser.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) {
|
||||
&& !extraUser.getSupertokensUserId().equals(primaryUserId)) {
|
||||
for (LoginMethod loginMethodExtra : extraUser.loginMethods) {
|
||||
if (loginMethodExtra.thirdParty != null &&
|
||||
loginMethodExtra.thirdParty.userId.equals(currLoginMethod.thirdParty.userId)
|
||||
&& loginMethodExtra.thirdParty.id.equals(currLoginMethod.thirdParty.id)) {
|
||||
loginMethodExtra.thirdParty.userId.equals(currLoginMethod.thirdPartyUserId)
|
||||
&& loginMethodExtra.thirdParty.id.equals(currLoginMethod.thirdPartyId)) {
|
||||
|
||||
throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
|
||||
extraUser.getSupertokensUserId(),
|
||||
|
|
@ -517,7 +509,7 @@ public class AuthRecipe {
|
|||
FeatureNotEnabledException, InputUserIdIsNotAPrimaryUserException,
|
||||
RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException {
|
||||
try {
|
||||
return linkAccounts(main, new AppIdentifier(null, null),
|
||||
return linkAccounts(main, ResourceDistributor.getAppForTesting().toAppIdentifier(),
|
||||
StorageLayer.getStorage(main), recipeUserId, primaryUserId);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
@ -588,10 +580,10 @@ public class AuthRecipe {
|
|||
}
|
||||
}
|
||||
|
||||
public static List<LinkAccountsBulkResult> linkMultipleAccounts(Main main, AppIdentifier appIdentifier,
|
||||
Storage storage, Map<String, String> recipeUserIdToPrimaryUserId,
|
||||
List<String> allDistinctEmailAddresses, List<String> allDistinctPhones,
|
||||
Map<String, String> allThirdpartyUserIdsToThirdpartyIds)
|
||||
public static void linkMultipleAccountsForBulkImport(Main main, AppIdentifier appIdentifier,
|
||||
Storage storage,
|
||||
List<BulkImportUser> users,
|
||||
List<AuthRecipeUserInfo> usersWithSameExtraData)
|
||||
throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException {
|
||||
|
||||
if (!Utils.isAccountLinkingEnabled(main, appIdentifier)) {
|
||||
|
|
@ -603,20 +595,13 @@ public class AuthRecipe {
|
|||
Map<String, Exception> errorByUserId = new HashMap<>();
|
||||
try {
|
||||
|
||||
List<LinkAccountsBulkResult> linkAccountsResults = authRecipeStorage.startTransaction(con -> {
|
||||
List<CanLinkAccountsBulkResult> canLinkAccounts = canLinkMultipleAccountsHelper(con, appIdentifier,
|
||||
authRecipeStorage, recipeUserIdToPrimaryUserId, allDistinctEmailAddresses, allDistinctPhones,
|
||||
allThirdpartyUserIdsToThirdpartyIds);
|
||||
List<LinkAccountsBulkResult> results = new ArrayList<>();
|
||||
authRecipeStorage.startTransaction(con -> {
|
||||
List<CanLinkAccountsBulkResult> canLinkAccounts = canLinkMultipleAccountsHelperForBulkImport(con, appIdentifier,
|
||||
authRecipeStorage, users, usersWithSameExtraData);
|
||||
Map<String, String> recipeUserByPrimaryUserNeedsLinking = new HashMap<>();
|
||||
if(!canLinkAccounts.isEmpty()){
|
||||
for(CanLinkAccountsBulkResult canLinkAccountsBulkResult : canLinkAccounts) {
|
||||
if (canLinkAccountsBulkResult.alreadyLinked) {
|
||||
results.add(new LinkAccountsBulkResult(
|
||||
canLinkAccountsBulkResult.authRecipeUserInfo, true, null));
|
||||
} else if(canLinkAccountsBulkResult.error != null) {
|
||||
results.add(new LinkAccountsBulkResult(
|
||||
canLinkAccountsBulkResult.authRecipeUserInfo, false, canLinkAccountsBulkResult.error)); // preparing to return the error
|
||||
if(!canLinkAccountsBulkResult.alreadyLinked && canLinkAccountsBulkResult.error != null) {
|
||||
errorByUserId.put(canLinkAccountsBulkResult.recipeUserId, canLinkAccountsBulkResult.error);
|
||||
} else {
|
||||
recipeUserByPrimaryUserNeedsLinking.put(canLinkAccountsBulkResult.recipeUserId, canLinkAccountsBulkResult.primaryUserId);
|
||||
|
|
@ -624,32 +609,14 @@ public class AuthRecipe {
|
|||
}
|
||||
// link the remaining
|
||||
authRecipeStorage.linkMultipleAccounts_Transaction(appIdentifier, con, recipeUserByPrimaryUserNeedsLinking);
|
||||
List<AuthRecipeUserInfo> linkedPrimaryUsers = getUsersById(appIdentifier, authRecipeStorage, new ArrayList<>(recipeUserByPrimaryUserNeedsLinking.values()));
|
||||
|
||||
for(AuthRecipeUserInfo linkedUser : linkedPrimaryUsers){
|
||||
results.add(new LinkAccountsBulkResult(linkedUser, false, null));
|
||||
}
|
||||
|
||||
authRecipeStorage.commitTransaction(con);
|
||||
}
|
||||
if(!errorByUserId.isEmpty()) {
|
||||
throw new StorageQueryException(new BulkImportBatchInsertException("link accounts errors", errorByUserId));
|
||||
}
|
||||
return results;
|
||||
return null;
|
||||
});
|
||||
|
||||
for(LinkAccountsBulkResult result : linkAccountsResults) {
|
||||
if (!result.wasAlreadyLinked) {
|
||||
io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult =
|
||||
io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(
|
||||
appIdentifier, authRecipeStorage,
|
||||
result.user.getSupertokensUserId(), UserIdType.SUPERTOKENS);
|
||||
// finally, we revoke all sessions of the recipeUser Id cause their user ID has changed.
|
||||
Session.revokeAllSessionsForUser(main, appIdentifier, authRecipeStorage,
|
||||
mappingResult == null ? result.user.getSupertokensUserId() : mappingResult.externalUserId, false);
|
||||
}
|
||||
}
|
||||
return linkAccountsResults;
|
||||
} catch (StorageTransactionLogicException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
|
|
@ -666,11 +633,11 @@ public class AuthRecipe {
|
|||
}
|
||||
|
||||
public static class LinkAccountsBulkResult {
|
||||
public final AuthRecipeUserInfo user;
|
||||
public final BulkImportUser user;
|
||||
public final boolean wasAlreadyLinked;
|
||||
public final Exception error;
|
||||
|
||||
public LinkAccountsBulkResult(AuthRecipeUserInfo user, boolean wasAlreadyLinked, Exception error) {
|
||||
public LinkAccountsBulkResult(BulkImportUser user, boolean wasAlreadyLinked, Exception error) {
|
||||
this.user = user;
|
||||
this.wasAlreadyLinked = wasAlreadyLinked;
|
||||
this.error = error;
|
||||
|
|
@ -682,7 +649,7 @@ public class AuthRecipe {
|
|||
String recipeUserId)
|
||||
throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException,
|
||||
RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, UnknownUserIdException {
|
||||
return canCreatePrimaryUser(new AppIdentifier(null, null), StorageLayer.getStorage(main), recipeUserId);
|
||||
return canCreatePrimaryUser(ResourceDistributor.getAppForTesting().toAppIdentifier(), StorageLayer.getStorage(main), recipeUserId);
|
||||
}
|
||||
|
||||
public static CreatePrimaryUserResult canCreatePrimaryUser(AppIdentifier appIdentifier,
|
||||
|
|
@ -799,116 +766,151 @@ public class AuthRecipe {
|
|||
return new CreatePrimaryUserResult(targetUser, false);
|
||||
}
|
||||
|
||||
private static List<CreatePrimaryUserBulkResult> canCreatePrimaryUsersHelper(TransactionConnection con,
|
||||
AppIdentifier appIdentifier,
|
||||
Storage storage,
|
||||
List<String> recipeUserIds,
|
||||
List<String> allDistinctEmails,
|
||||
List<String> allPhones,
|
||||
Map<String, String> thirdpartyUserIdToThirdpartyId)
|
||||
private static CreatePrimaryUsersResultHolder canCreatePrimaryUsersHelperForBulkImport(TransactionConnection con,
|
||||
AppIdentifier appIdentifier,
|
||||
Storage storage,
|
||||
List<BulkImportUser> bulkImportUsers)
|
||||
throws StorageQueryException, UnknownUserIdException{
|
||||
|
||||
AuthRecipeSQLStorage authRecipeStorage = StorageUtils.getAuthRecipeStorage(storage);
|
||||
List<AuthRecipeUserInfo> targetUsers = authRecipeStorage.getPrimaryUsersByIds_Transaction(appIdentifier, con,
|
||||
recipeUserIds);
|
||||
if (targetUsers == null || targetUsers.isEmpty()) {
|
||||
|
||||
if (bulkImportUsers == null || bulkImportUsers.isEmpty()) {
|
||||
throw new UnknownUserIdException();
|
||||
}
|
||||
|
||||
DistinctAuthIdentifiers mailPhoneThirdParty = getDistinctAuthIdentifiers(bulkImportUsers);
|
||||
|
||||
List<CreatePrimaryUserBulkResult> results = new ArrayList<>();
|
||||
List<AuthRecipeUserInfo> allUsersWithProvidedExtraData =
|
||||
List.of(authRecipeStorage.
|
||||
listPrimaryUsersByMultipleEmailsOrPhoneNumbersOrThirdparty_Transaction(appIdentifier, con,
|
||||
allDistinctEmails, allPhones, thirdpartyUserIdToThirdpartyId));
|
||||
new ArrayList<>(mailPhoneThirdParty.allEmails), new ArrayList<>(mailPhoneThirdParty.allPhoneNumber),
|
||||
mailPhoneThirdParty.allThirdParty)); // this is multiple - not so cheap DB query, but we need to do it
|
||||
|
||||
for(int i = 0; i < targetUsers.size(); i++) {
|
||||
AuthRecipeUserInfo targetUser = targetUsers.get(i);
|
||||
if (targetUser.isPrimaryUser) {
|
||||
if (targetUser.getSupertokensUserId()
|
||||
.equals(recipeUserIds.get(i))) {
|
||||
results.add(new CreatePrimaryUserBulkResult(targetUser, true, null));
|
||||
} else {
|
||||
results.add(new CreatePrimaryUserBulkResult(targetUser, false,
|
||||
new RecipeUserIdAlreadyLinkedWithPrimaryUserIdException(targetUser.getSupertokensUserId(),
|
||||
"This user ID is already linked to another user ID")));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (BulkImportUser targetUser : bulkImportUsers) {
|
||||
BulkImportUser.LoginMethod primaryLoginMethod = BulkImportUserUtils.getPrimaryLoginMethod(targetUser);
|
||||
|
||||
|
||||
// this means that the user has only one login method since it's not a primary user
|
||||
// nor is it linked to a primary user
|
||||
assert (targetUser.loginMethods.length == 1);
|
||||
LoginMethod loginMethod = targetUser.loginMethods[0];
|
||||
boolean errorFound = false;
|
||||
|
||||
for (String tenantId : targetUser.tenantIds) {
|
||||
if (loginMethod.email != null) {
|
||||
List<AuthRecipeUserInfo> usersWithSameEmail = allUsersWithProvidedExtraData.stream().filter(authRecipeUserInfo -> Arrays.stream(
|
||||
authRecipeUserInfo.loginMethods).map(loginMethod1 -> loginMethod1.email).collect(Collectors.toList()).contains(loginMethod.email)).collect(
|
||||
Collectors.toList());
|
||||
for (AuthRecipeUserInfo user : usersWithSameEmail) {
|
||||
if (!user.tenantIds.contains(tenantId)) {
|
||||
continue;
|
||||
}
|
||||
if (user.isPrimaryUser) {
|
||||
results.add(new CreatePrimaryUserBulkResult(targetUser, false,
|
||||
new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
|
||||
user.getSupertokensUserId(),
|
||||
"This user's email is already associated with another user ID")));
|
||||
errorFound = true;
|
||||
break;
|
||||
for (BulkImportUser.LoginMethod loginMethod : targetUser.loginMethods) {
|
||||
// note here: account takeover risk checks are done in the sdk. The situation in which someone registers
|
||||
// for example with a thirparty which also verifies email address and later someone else tries to register
|
||||
// with the same email address but with emailpassword is not handled here. This is because the sdk
|
||||
// will handle this. In the bulk import we have no means to check this.
|
||||
boolean errorFound = false;
|
||||
for (String tenantId : loginMethod.tenantIds) {
|
||||
if (loginMethod.email != null) {
|
||||
List<AuthRecipeUserInfo> usersWithSameEmail = allUsersWithProvidedExtraData.stream()
|
||||
.filter(authRecipeUserInfo -> Arrays.stream(
|
||||
authRecipeUserInfo.loginMethods).map(loginMethod1 -> loginMethod1.email)
|
||||
.collect(Collectors.toList()).contains(loginMethod.email)).collect(
|
||||
Collectors.toList());
|
||||
for (AuthRecipeUserInfo user : usersWithSameEmail) {
|
||||
if (!user.tenantIds.contains(tenantId)) {
|
||||
continue;
|
||||
}
|
||||
if (user.isPrimaryUser) {
|
||||
results.add(new CreatePrimaryUserBulkResult(targetUser, primaryLoginMethod, false,
|
||||
new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
|
||||
user.getSupertokensUserId(),
|
||||
"This user's email is already associated with another user ID")));
|
||||
errorFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (loginMethod.phoneNumber != null) {
|
||||
List<AuthRecipeUserInfo> usersWithSamePhoneNumber = allUsersWithProvidedExtraData.stream().filter(authRecipeUserInfo -> Arrays.stream(
|
||||
authRecipeUserInfo.loginMethods).map(loginMethod1 -> loginMethod1.phoneNumber).collect(Collectors.toList()).contains(loginMethod.phoneNumber)).collect(
|
||||
Collectors.toList());
|
||||
for (AuthRecipeUserInfo user : usersWithSamePhoneNumber) {
|
||||
if (!user.tenantIds.contains(tenantId)) {
|
||||
continue;
|
||||
}
|
||||
if (user.isPrimaryUser) {
|
||||
results.add(new CreatePrimaryUserBulkResult(targetUser, false,
|
||||
new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
|
||||
user.getSupertokensUserId(),
|
||||
"This user's phone number is already associated with another user" +
|
||||
" ID")));
|
||||
errorFound = true;
|
||||
break;
|
||||
if (loginMethod.phoneNumber != null) {
|
||||
List<AuthRecipeUserInfo> usersWithSamePhoneNumber = allUsersWithProvidedExtraData.stream()
|
||||
.filter(authRecipeUserInfo -> Arrays.stream(
|
||||
authRecipeUserInfo.loginMethods).map(loginMethod1 -> loginMethod1.phoneNumber)
|
||||
.collect(Collectors.toList()).contains(loginMethod.phoneNumber)).collect(
|
||||
Collectors.toList());
|
||||
for (AuthRecipeUserInfo user : usersWithSamePhoneNumber) {
|
||||
if (!user.tenantIds.contains(tenantId)) {
|
||||
continue;
|
||||
}
|
||||
if (user.isPrimaryUser) {
|
||||
results.add(new CreatePrimaryUserBulkResult(targetUser, primaryLoginMethod, false,
|
||||
new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
|
||||
user.getSupertokensUserId(),
|
||||
"This user's phone number is already associated with another user" +
|
||||
" ID")));
|
||||
errorFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (loginMethod.thirdParty != null) {
|
||||
List<AuthRecipeUserInfo> extraUsersWithThirdParty = allUsersWithProvidedExtraData.stream().filter(authRecipeUserInfo -> Arrays.stream(
|
||||
authRecipeUserInfo.loginMethods).anyMatch(loginMethod1 -> loginMethod1.thirdParty != null)).collect(Collectors.toList());
|
||||
for(AuthRecipeUserInfo extraUser : extraUsersWithThirdParty) {
|
||||
if(extraUser.isPrimaryUser && extraUser.tenantIds.contains(tenantId)) {
|
||||
for (LoginMethod loginMethodExtra : extraUser.loginMethods) {
|
||||
if (loginMethodExtra.thirdParty != null &&
|
||||
loginMethodExtra.thirdParty.userId.equals(loginMethod.thirdParty.userId)
|
||||
&& loginMethodExtra.thirdParty.id.equals(loginMethod.thirdParty.id)) {
|
||||
if (loginMethod.thirdPartyId != null && loginMethod.thirdPartyUserId != null) {
|
||||
List<AuthRecipeUserInfo> extraUsersWithThirdParty = allUsersWithProvidedExtraData.stream()
|
||||
.filter(authRecipeUserInfo -> Arrays.stream(
|
||||
authRecipeUserInfo.loginMethods)
|
||||
.anyMatch(loginMethod1 -> loginMethod1.thirdParty != null))
|
||||
.collect(Collectors.toList());
|
||||
for (AuthRecipeUserInfo extraUser : extraUsersWithThirdParty) {
|
||||
if (extraUser.isPrimaryUser && extraUser.tenantIds.contains(tenantId)) {
|
||||
for (LoginMethod loginMethodExtra : extraUser.loginMethods) {
|
||||
if (loginMethodExtra.thirdParty != null &&
|
||||
loginMethodExtra.thirdParty.userId.equals(loginMethod.thirdPartyUserId)
|
||||
&& loginMethodExtra.thirdParty.id.equals(loginMethod.thirdPartyId)) {
|
||||
|
||||
results.add(new CreatePrimaryUserBulkResult(targetUser, false,
|
||||
new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
|
||||
extraUser.getSupertokensUserId(),
|
||||
"This user's third party login is already associated with another" +
|
||||
" user ID")));
|
||||
errorFound = true;
|
||||
break;
|
||||
results.add(
|
||||
new CreatePrimaryUserBulkResult(targetUser, primaryLoginMethod, false,
|
||||
new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(
|
||||
extraUser.getSupertokensUserId(),
|
||||
"This user's third party login is already associated with another" +
|
||||
" user ID")));
|
||||
errorFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!errorFound){
|
||||
results.add(new CreatePrimaryUserBulkResult(targetUser, false, null));
|
||||
if (!errorFound) {
|
||||
results.add(new CreatePrimaryUserBulkResult(targetUser, primaryLoginMethod, false, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
CreatePrimaryUsersResultHolder resultHolder = new CreatePrimaryUsersResultHolder();
|
||||
resultHolder.createPrimaryUserBulkResults = results;
|
||||
resultHolder.usersWithSameExtraData = allUsersWithProvidedExtraData;
|
||||
return resultHolder;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static DistinctAuthIdentifiers getDistinctAuthIdentifiers(List<BulkImportUser> bulkImportUsers) {
|
||||
Set<String> allEmails = new HashSet<>();
|
||||
Set<String> allPhoneNumber = new HashSet<>();
|
||||
Map<String, String> allThirdParty = new HashMap<>();
|
||||
for (BulkImportUser user : bulkImportUsers) {
|
||||
for (BulkImportUser.LoginMethod loginMethod : user.loginMethods) {
|
||||
if (loginMethod.email != null) {
|
||||
allEmails.add(loginMethod.email);
|
||||
}
|
||||
if (loginMethod.phoneNumber != null) {
|
||||
allPhoneNumber.add(loginMethod.phoneNumber);
|
||||
}
|
||||
if (loginMethod.thirdPartyId != null && loginMethod.thirdPartyUserId != null) {
|
||||
allThirdParty.put(loginMethod.thirdPartyUserId, loginMethod.thirdPartyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
DistinctAuthIdentifiers mailPhoneThirdparty = new DistinctAuthIdentifiers(allEmails, allPhoneNumber, allThirdParty);
|
||||
return mailPhoneThirdparty;
|
||||
}
|
||||
|
||||
private static class DistinctAuthIdentifiers {
|
||||
public final Set<String> allEmails;
|
||||
public final Set<String> allPhoneNumber;
|
||||
public final Map<String, String> allThirdParty;
|
||||
|
||||
public DistinctAuthIdentifiers(Set<String> allEmails, Set<String> allPhoneNumber, Map<String, String> allThirdParty) {
|
||||
this.allEmails = allEmails;
|
||||
this.allPhoneNumber = allPhoneNumber;
|
||||
this.allThirdParty = allThirdParty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -919,7 +921,7 @@ public class AuthRecipe {
|
|||
RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, UnknownUserIdException,
|
||||
FeatureNotEnabledException {
|
||||
try {
|
||||
return createPrimaryUser(main, new AppIdentifier(null, null), StorageLayer.getStorage(main), recipeUserId);
|
||||
return createPrimaryUser(main, ResourceDistributor.getAppForTesting().toAppIdentifier(), StorageLayer.getStorage(main), recipeUserId);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
|
@ -973,15 +975,19 @@ public class AuthRecipe {
|
|||
}
|
||||
}
|
||||
|
||||
public static List<CreatePrimaryUserBulkResult> createPrimaryUsers(Main main,
|
||||
AppIdentifier appIdentifier,
|
||||
Storage storage,
|
||||
List<String> recipeUserIds,
|
||||
List<String> allDistinctEmails,
|
||||
List<String> allDistinctPhones,
|
||||
Map<String, String> thirdpartyUserIdsToThirdpartyIds)
|
||||
//helper class to return together the results of primary user creation and the users with the same extradata (email, phone, etc)
|
||||
public static class CreatePrimaryUsersResultHolder {
|
||||
public List<CreatePrimaryUserBulkResult> createPrimaryUserBulkResults;
|
||||
public List<AuthRecipeUserInfo> usersWithSameExtraData;
|
||||
}
|
||||
|
||||
public static CreatePrimaryUsersResultHolder createPrimaryUsersForBulkImport(Main main,
|
||||
AppIdentifier appIdentifier,
|
||||
Storage storage,
|
||||
List<BulkImportUser> bulkImportUsers)
|
||||
throws StorageQueryException, TenantOrAppNotFoundException,
|
||||
FeatureNotEnabledException {
|
||||
|
||||
if (!Utils.isAccountLinkingEnabled(main, appIdentifier)) {
|
||||
throw new FeatureNotEnabledException(
|
||||
"Account linking feature is not enabled for this app. Please contact support to enable it.");
|
||||
|
|
@ -993,21 +999,22 @@ public class AuthRecipe {
|
|||
return authRecipeStorage.startTransaction(con -> {
|
||||
|
||||
try {
|
||||
List<CreatePrimaryUserBulkResult> results = canCreatePrimaryUsersHelper(con, appIdentifier, authRecipeStorage,
|
||||
recipeUserIds, allDistinctEmails, allDistinctPhones, thirdpartyUserIdsToThirdpartyIds);
|
||||
CreatePrimaryUsersResultHolder resultHolder = canCreatePrimaryUsersHelperForBulkImport(con, appIdentifier, authRecipeStorage,
|
||||
bulkImportUsers);
|
||||
List<CreatePrimaryUserBulkResult> results = resultHolder.createPrimaryUserBulkResults;
|
||||
List<CreatePrimaryUserBulkResult> canMakePrimaryUsers = new ArrayList<>();
|
||||
for(CreatePrimaryUserBulkResult result : results) {
|
||||
if (result.wasAlreadyAPrimaryUser) {
|
||||
continue;
|
||||
}
|
||||
if(result.error != null) {
|
||||
errorsByUserId.put(result.user.getSupertokensUserId(), result.error);
|
||||
errorsByUserId.put(result.user.id, result.error);
|
||||
continue;
|
||||
}
|
||||
canMakePrimaryUsers.add(result);
|
||||
}
|
||||
authRecipeStorage.makePrimaryUsers_Transaction(appIdentifier, con,
|
||||
canMakePrimaryUsers.stream().map(canMakePrimaryUser -> canMakePrimaryUser.user.getSupertokensUserId()).collect(
|
||||
canMakePrimaryUsers.stream().map(canMakePrimaryUser -> canMakePrimaryUser.user.id).collect(
|
||||
Collectors.toList()));
|
||||
|
||||
authRecipeStorage.commitTransaction(con);
|
||||
|
|
@ -1017,17 +1024,18 @@ public class AuthRecipe {
|
|||
continue;
|
||||
}
|
||||
if(result.error != null) {
|
||||
errorsByUserId.put(result.user.getSupertokensUserId(), result.error);
|
||||
errorsByUserId.put(result.user.id, result.error);
|
||||
continue;
|
||||
}
|
||||
result.user.isPrimaryUser = true;
|
||||
result.primaryLoginMethod.isPrimary = true;
|
||||
result.user.primaryUserId = result.primaryLoginMethod.superTokensUserId;
|
||||
}
|
||||
|
||||
if(!errorsByUserId.isEmpty()) {
|
||||
throw new StorageTransactionLogicException(new BulkImportBatchInsertException("create primary users errors", errorsByUserId));
|
||||
}
|
||||
|
||||
return results;
|
||||
return resultHolder;
|
||||
} catch (UnknownUserIdException e) {
|
||||
throw new StorageTransactionLogicException(e);
|
||||
}
|
||||
|
|
@ -1037,76 +1045,112 @@ public class AuthRecipe {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public static AuthRecipeUserInfo[] getUsersByAccountInfo(TenantIdentifier tenantIdentifier,
|
||||
Storage storage,
|
||||
boolean doUnionOfAccountInfo, String email,
|
||||
String phoneNumber, String thirdPartyId,
|
||||
String thirdPartyUserId)
|
||||
String thirdPartyUserId,
|
||||
String webauthnCredentialId)
|
||||
throws StorageQueryException {
|
||||
Set<AuthRecipeUserInfo> result = loadAuthRecipeUserInfosByVariousIds(
|
||||
tenantIdentifier, storage, email, phoneNumber, thirdPartyId, thirdPartyUserId, webauthnCredentialId);
|
||||
|
||||
if (doUnionOfAccountInfo) {
|
||||
return mergeAuthRecipeUserInfosResultWithORMatch(result); // matches any of the provided: email, thirdparty, phone number, webauthnCredential
|
||||
} else {
|
||||
return mergeAuthRecipeUserInfosResultWithANDMatch(email, phoneNumber, thirdPartyId, thirdPartyUserId, webauthnCredentialId,
|
||||
result); // matches all the provided: email, thirdparty, phone number, webauthnCredential
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static AuthRecipeUserInfo[] mergeAuthRecipeUserInfosResultWithANDMatch(String email, String phoneNumber,
|
||||
String thirdPartyId, String thirdPartyUserId,
|
||||
String webauthnCredentialId,
|
||||
Set<AuthRecipeUserInfo> result) {
|
||||
List<AuthRecipeUserInfo> finalList = new ArrayList<>();
|
||||
for (AuthRecipeUserInfo user : result) {
|
||||
boolean emailMatch = email == null;
|
||||
boolean phoneNumberMatch = phoneNumber == null;
|
||||
boolean thirdPartyMatch = thirdPartyId == null;
|
||||
boolean webauthnCredentialIdMatch = webauthnCredentialId == null;
|
||||
for (LoginMethod lM : user.loginMethods) {
|
||||
if (email != null && email.equals(lM.email)) {
|
||||
emailMatch = true;
|
||||
}
|
||||
if (phoneNumber != null && phoneNumber.equals(lM.phoneNumber)) {
|
||||
phoneNumberMatch = true;
|
||||
}
|
||||
if (thirdPartyId != null &&
|
||||
(new LoginMethod.ThirdParty(thirdPartyId, thirdPartyUserId)).equals(lM.thirdParty)) {
|
||||
thirdPartyMatch = true;
|
||||
}
|
||||
if(webauthnCredentialId != null
|
||||
&& lM.webauthN != null
|
||||
&& lM.webauthN.credentialIds.contains(webauthnCredentialId)){
|
||||
webauthnCredentialIdMatch = true;
|
||||
}
|
||||
}
|
||||
if (emailMatch && phoneNumberMatch && thirdPartyMatch && webauthnCredentialIdMatch) {
|
||||
finalList.add(user);
|
||||
}
|
||||
}
|
||||
finalList.sort((o1, o2) -> {
|
||||
if (o1.timeJoined < o2.timeJoined) {
|
||||
return -1;
|
||||
} else if (o1.timeJoined > o2.timeJoined) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return finalList.toArray(new AuthRecipeUserInfo[0]);
|
||||
}
|
||||
|
||||
private static AuthRecipeUserInfo[] mergeAuthRecipeUserInfosResultWithORMatch(Set<AuthRecipeUserInfo> result) {
|
||||
AuthRecipeUserInfo[] finalResult = result.toArray(new AuthRecipeUserInfo[0]);
|
||||
return Arrays.stream(finalResult).sorted((o1, o2) -> {
|
||||
if (o1.timeJoined < o2.timeJoined) {
|
||||
return -1;
|
||||
} else if (o1.timeJoined > o2.timeJoined) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}).toArray(AuthRecipeUserInfo[]::new);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Set<AuthRecipeUserInfo> loadAuthRecipeUserInfosByVariousIds(TenantIdentifier tenantIdentifier, Storage storage,
|
||||
String email, String phoneNumber, String thirdPartyId,
|
||||
String thirdPartyUserId, String webauthnCredentialId)
|
||||
throws StorageQueryException {
|
||||
Set<AuthRecipeUserInfo> result = new HashSet<>();
|
||||
|
||||
AuthRecipeSQLStorage authRecipeStorage = StorageUtils.getAuthRecipeStorage(storage);
|
||||
if (email != null) {
|
||||
AuthRecipeUserInfo[] users = StorageUtils.getAuthRecipeStorage(storage)
|
||||
AuthRecipeUserInfo[] users = authRecipeStorage
|
||||
.listPrimaryUsersByEmail(tenantIdentifier, email);
|
||||
result.addAll(List.of(users));
|
||||
}
|
||||
if (phoneNumber != null) {
|
||||
AuthRecipeUserInfo[] users = StorageUtils.getAuthRecipeStorage(storage)
|
||||
AuthRecipeUserInfo[] users = authRecipeStorage
|
||||
.listPrimaryUsersByPhoneNumber(tenantIdentifier, phoneNumber);
|
||||
result.addAll(List.of(users));
|
||||
}
|
||||
if (thirdPartyId != null && thirdPartyUserId != null) {
|
||||
AuthRecipeUserInfo user = StorageUtils.getAuthRecipeStorage(storage)
|
||||
AuthRecipeUserInfo user = authRecipeStorage
|
||||
.getPrimaryUserByThirdPartyInfo(tenantIdentifier, thirdPartyId, thirdPartyUserId);
|
||||
if (user != null) {
|
||||
result.add(user);
|
||||
}
|
||||
}
|
||||
|
||||
if (doUnionOfAccountInfo) {
|
||||
AuthRecipeUserInfo[] finalResult = result.toArray(new AuthRecipeUserInfo[0]);
|
||||
return Arrays.stream(finalResult).sorted((o1, o2) -> {
|
||||
if (o1.timeJoined < o2.timeJoined) {
|
||||
return -1;
|
||||
} else if (o1.timeJoined > o2.timeJoined) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}).toArray(AuthRecipeUserInfo[]::new);
|
||||
} else {
|
||||
List<AuthRecipeUserInfo> finalList = new ArrayList<>();
|
||||
for (AuthRecipeUserInfo user : result) {
|
||||
boolean emailMatch = email == null;
|
||||
boolean phoneNumberMatch = phoneNumber == null;
|
||||
boolean thirdPartyMatch = thirdPartyId == null;
|
||||
for (LoginMethod lM : user.loginMethods) {
|
||||
if (email != null && email.equals(lM.email)) {
|
||||
emailMatch = true;
|
||||
}
|
||||
if (phoneNumber != null && phoneNumber.equals(lM.phoneNumber)) {
|
||||
phoneNumberMatch = true;
|
||||
}
|
||||
if (thirdPartyId != null &&
|
||||
(new LoginMethod.ThirdParty(thirdPartyId, thirdPartyUserId)).equals(lM.thirdParty)) {
|
||||
thirdPartyMatch = true;
|
||||
}
|
||||
}
|
||||
if (emailMatch && phoneNumberMatch && thirdPartyMatch) {
|
||||
finalList.add(user);
|
||||
}
|
||||
if(webauthnCredentialId != null){
|
||||
AuthRecipeUserInfo user = authRecipeStorage
|
||||
.getPrimaryUserByWebauthNCredentialId(tenantIdentifier, webauthnCredentialId);
|
||||
if (user != null) {
|
||||
result.add(user);
|
||||
}
|
||||
finalList.sort((o1, o2) -> {
|
||||
if (o1.timeJoined < o2.timeJoined) {
|
||||
return -1;
|
||||
} else if (o1.timeJoined > o2.timeJoined) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return finalList.toArray(new AuthRecipeUserInfo[0]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static long getUsersCountForTenant(TenantIdentifier tenantIdentifier,
|
||||
|
|
@ -1138,7 +1182,7 @@ public class AuthRecipe {
|
|||
RECIPE_ID[] includeRecipeIds) throws StorageQueryException {
|
||||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getUsersCountForTenant(TenantIdentifier.BASE_TENANT, storage, includeRecipeIds);
|
||||
return getUsersCountForTenant(ResourceDistributor.getAppForTesting(), storage, includeRecipeIds);
|
||||
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
|
@ -1188,7 +1232,7 @@ public class AuthRecipe {
|
|||
throws StorageQueryException, UserPaginationToken.InvalidTokenException {
|
||||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getUsers(TenantIdentifier.BASE_TENANT, storage,
|
||||
return getUsers(ResourceDistributor.getAppForTesting(), storage,
|
||||
limit, timeJoinedOrder, paginationToken, includeRecipeIds, dashboardSearchTags);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -1346,7 +1390,7 @@ public class AuthRecipe {
|
|||
public static void deleteUser(Main main, String userId, boolean removeAllLinkedAccounts)
|
||||
throws StorageQueryException, StorageTransactionLogicException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
AppIdentifier appIdentifier = new AppIdentifier(null, null);
|
||||
AppIdentifier appIdentifier = ResourceDistributor.getAppForTesting().toAppIdentifier();
|
||||
UserIdMapping mapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(appIdentifier,
|
||||
storage, userId, UserIdType.ANY);
|
||||
|
||||
|
|
@ -1357,7 +1401,7 @@ public class AuthRecipe {
|
|||
public static void deleteUser(Main main, String userId)
|
||||
throws StorageQueryException, StorageTransactionLogicException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
AppIdentifier appIdentifier = new AppIdentifier(null, null);
|
||||
AppIdentifier appIdentifier = ResourceDistributor.getAppForTesting().toAppIdentifier();
|
||||
UserIdMapping mapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(appIdentifier,
|
||||
storage, userId, UserIdType.ANY);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0 (the
|
||||
* "License") as published by the Apache Software Foundation.
|
||||
*
|
||||
* You may not use this file except in compliance with the License. You may
|
||||
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package io.supertokens.authRecipe.exception;
|
||||
|
||||
public class BulkImportRecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException extends Exception {
|
||||
public final String recipeUserId;
|
||||
|
||||
public BulkImportRecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(String recipeUserId) {
|
||||
super("The recipe user id '" + recipeUserId + "' is already linked with another primary user id");
|
||||
this.recipeUserId = recipeUserId;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ import io.supertokens.multitenancy.Multitenancy;
|
|||
import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithEmailAlreadyExistsException;
|
||||
import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithPhoneNumberAlreadyExistsException;
|
||||
import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException;
|
||||
import io.supertokens.output.Logging;
|
||||
import io.supertokens.passwordless.Passwordless;
|
||||
import io.supertokens.pluginInterface.Storage;
|
||||
import io.supertokens.pluginInterface.StorageUtils;
|
||||
|
|
@ -73,6 +74,7 @@ import io.supertokens.usermetadata.UserMetadata;
|
|||
import io.supertokens.userroles.UserRoles;
|
||||
import io.supertokens.utils.Utils;
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -94,8 +96,6 @@ public class BulkImport {
|
|||
public static final int GET_USERS_DEFAULT_LIMIT = 100;
|
||||
// Maximum number of users that can be deleted in a single operation
|
||||
public static final int DELETE_USERS_MAX_LIMIT = 500;
|
||||
// Number of users to process in a single batch of ProcessBulkImportUsers Cron Job
|
||||
public static final int PROCESS_USERS_BATCH_SIZE = 8000;
|
||||
// Time interval in seconds between two consecutive runs of ProcessBulkImportUsers Cron Job
|
||||
public static final int PROCESS_USERS_INTERVAL_SECONDS = 5*60; // 5 minutes
|
||||
private static final Logger log = LoggerFactory.getLogger(BulkImport.class);
|
||||
|
|
@ -173,7 +173,7 @@ public class BulkImport {
|
|||
|
||||
SQLStorage bulkImportProxyStorage = (SQLStorage) getBulkImportProxyStorage(main, firstTenantIdentifier);
|
||||
|
||||
LoginMethod primaryLM = getPrimaryLoginMethod(user);
|
||||
LoginMethod primaryLM = BulkImportUserUtils.getPrimaryLoginMethod(user);
|
||||
|
||||
try {
|
||||
return bulkImportProxyStorage.startTransaction(con -> {
|
||||
|
|
@ -210,13 +210,28 @@ public class BulkImport {
|
|||
Storage bulkImportProxyStorage, List<BulkImportUser> users, Storage[] allStoragesForApp)
|
||||
throws StorageTransactionLogicException {
|
||||
try {
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing login methods..");
|
||||
processUsersLoginMethods(main, appIdentifier, bulkImportProxyStorage, users);
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing login methods DONE");
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating Primary users and linking accounts..");
|
||||
createPrimaryUsersAndLinkAccounts(main, appIdentifier, bulkImportProxyStorage, users);
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating Primary users and linking accounts DONE");
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating user id mappings..");
|
||||
createMultipleUserIdMapping(appIdentifier, users, allStoragesForApp);
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating user id mappings DONE");
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Verifying email addresses..");
|
||||
verifyMultipleEmailForAllLoginMethods(appIdentifier, bulkImportProxyStorage, users);
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Verifying email addresses DONE");
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating TOTP devices..");
|
||||
createMultipleTotpDevices(main, appIdentifier, bulkImportProxyStorage, users);
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating TOTP devices DONE");
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating user metadata..");
|
||||
createMultipleUserMetadata(appIdentifier, bulkImportProxyStorage, users);
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating user metadata DONE");
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating user roles..");
|
||||
createMultipleUserRoles(main, appIdentifier, bulkImportProxyStorage, users);
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Creating user roles DONE");
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Effective processUsersImportSteps DONE");
|
||||
} catch ( StorageQueryException | FeatureNotEnabledException |
|
||||
TenantOrAppNotFoundException e) {
|
||||
throw new StorageTransactionLogicException(e);
|
||||
|
|
@ -226,6 +241,7 @@ public class BulkImport {
|
|||
public static void processUsersLoginMethods(Main main, AppIdentifier appIdentifier, Storage storage,
|
||||
List<BulkImportUser> users) throws StorageTransactionLogicException {
|
||||
//sort login methods together
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Sorting login methods by recipeId..");
|
||||
Map<String, List<LoginMethod>> sortedLoginMethods = new HashMap<>();
|
||||
for (BulkImportUser user: users) {
|
||||
for(LoginMethod loginMethod : user.loginMethods){
|
||||
|
|
@ -237,19 +253,25 @@ public class BulkImport {
|
|||
}
|
||||
|
||||
List<ImportUserBase> importedUsers = new ArrayList<>();
|
||||
if (sortedLoginMethods.containsKey("emailpassword")) {
|
||||
importedUsers.addAll(
|
||||
processEmailPasswordLoginMethods(main, storage, sortedLoginMethods.get("emailpassword"),
|
||||
if (sortedLoginMethods.containsKey("emailpassword")) {
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing emailpassword login methods..");
|
||||
importedUsers.addAll(
|
||||
processEmailPasswordLoginMethods(main, storage, sortedLoginMethods.get("emailpassword"),
|
||||
appIdentifier));
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing emailpassword login methods DONE");
|
||||
}
|
||||
if (sortedLoginMethods.containsKey("thirdparty")) {
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing thirdparty login methods..");
|
||||
importedUsers.addAll(
|
||||
processThirdpartyLoginMethods(main, storage, sortedLoginMethods.get("thirdparty"),
|
||||
appIdentifier));
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing thirdparty login methods DONE");
|
||||
}
|
||||
if (sortedLoginMethods.containsKey("passwordless")) {
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing passwordless login methods..");
|
||||
importedUsers.addAll(processPasswordlessLoginMethods(main, appIdentifier, storage,
|
||||
sortedLoginMethods.get("passwordless")));
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "Processing passwordless login methods DONE");
|
||||
}
|
||||
Set<String> actualKeys = new HashSet<>(sortedLoginMethods.keySet());
|
||||
List.of("emailpassword", "thirdparty", "passwordless").forEach(actualKeys::remove);
|
||||
|
|
@ -280,20 +302,18 @@ public class BulkImport {
|
|||
try {
|
||||
List<PasswordlessImportUser> usersToImport = new ArrayList<>();
|
||||
for (LoginMethod loginMethod : loginMethods) {
|
||||
String userId = Utils.getUUID();
|
||||
TenantIdentifier tenantIdentifierForLoginMethod = new TenantIdentifier(
|
||||
appIdentifier.getConnectionUriDomain(),
|
||||
appIdentifier.getAppId(), loginMethod.tenantIds.get(
|
||||
0)); // the cron runs per app. The app stays the same, the tenant can change
|
||||
usersToImport.add(new PasswordlessImportUser(userId, loginMethod.phoneNumber,
|
||||
usersToImport.add(new PasswordlessImportUser(loginMethod.superTokensUserId, loginMethod.phoneNumber,
|
||||
loginMethod.email, tenantIdentifierForLoginMethod, loginMethod.timeJoinedInMSSinceEpoch));
|
||||
loginMethod.superTokensUserId = userId;
|
||||
}
|
||||
|
||||
Passwordless.createPasswordlessUsers(storage, usersToImport);
|
||||
|
||||
return usersToImport;
|
||||
} catch (StorageQueryException | StorageTransactionLogicException e) {
|
||||
Logging.debug(main, TenantIdentifier.BASE_TENANT, "exception: " + e.getMessage());
|
||||
if (e.getCause() instanceof BulkImportBatchInsertException) {
|
||||
Map<String, Exception> errorsByPosition = ((BulkImportBatchInsertException) e.getCause()).exceptionByUserId;
|
||||
for (String userid : errorsByPosition.keySet()) {
|
||||
|
|
@ -327,13 +347,11 @@ public class BulkImport {
|
|||
try {
|
||||
List<ThirdPartyImportUser> usersToImport = new ArrayList<>();
|
||||
for (LoginMethod loginMethod: loginMethods){
|
||||
String userId = Utils.getUUID();
|
||||
TenantIdentifier tenantIdentifierForLoginMethod = new TenantIdentifier(appIdentifier.getConnectionUriDomain(),
|
||||
appIdentifier.getAppId(), loginMethod.tenantIds.get(0)); // the cron runs per app. The app stays the same, the tenant can change
|
||||
|
||||
usersToImport.add(new ThirdPartyImportUser(loginMethod.email, userId, loginMethod.thirdPartyId,
|
||||
usersToImport.add(new ThirdPartyImportUser(loginMethod.email, loginMethod.superTokensUserId, loginMethod.thirdPartyId,
|
||||
loginMethod.thirdPartyUserId, tenantIdentifierForLoginMethod, loginMethod.timeJoinedInMSSinceEpoch));
|
||||
loginMethod.superTokensUserId = userId;
|
||||
}
|
||||
ThirdParty.createMultipleThirdPartyUsers(storage, usersToImport);
|
||||
|
||||
|
|
@ -380,10 +398,8 @@ public class BulkImport {
|
|||
.createHashWithSalt(tenantIdentifierForLoginMethod.toAppIdentifier(), emailPasswordLoginMethod.plainTextPassword);
|
||||
}
|
||||
emailPasswordLoginMethod.passwordHash = passwordHash;
|
||||
String userId = Utils.getUUID();
|
||||
usersToImport.add(new EmailPasswordImportUser(userId, emailPasswordLoginMethod.email,
|
||||
usersToImport.add(new EmailPasswordImportUser(emailPasswordLoginMethod.superTokensUserId, emailPasswordLoginMethod.email,
|
||||
emailPasswordLoginMethod.passwordHash, tenantIdentifierForLoginMethod, emailPasswordLoginMethod.timeJoinedInMSSinceEpoch));
|
||||
emailPasswordLoginMethod.superTokensUserId = userId;
|
||||
}
|
||||
|
||||
EmailPassword.createMultipleUsersWithPasswordHash(storage, usersToImport);
|
||||
|
|
@ -419,7 +435,7 @@ public class BulkImport {
|
|||
|
||||
TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifier.getConnectionUriDomain(),
|
||||
appIdentifier.getAppId(), tenantId);
|
||||
Multitenancy.addUserIdToTenant(main, tenantIdentifier, storage, lm.getSuperTokenOrExternalUserId());
|
||||
Multitenancy.addUserIdToTenant(main, tenantIdentifier, storage, lm.superTokensUserId);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new StorageTransactionLogicException(new Exception("E009: " + e.getMessage()));
|
||||
} catch (StorageQueryException e) {
|
||||
|
|
@ -465,31 +481,15 @@ public class BulkImport {
|
|||
List<BulkImportUser> users)
|
||||
throws StorageTransactionLogicException, StorageQueryException, FeatureNotEnabledException,
|
||||
TenantOrAppNotFoundException {
|
||||
List<String> userIds =
|
||||
users.stream()
|
||||
.map(bulkImportUser -> getPrimaryLoginMethod(bulkImportUser).getSuperTokenOrExternalUserId())
|
||||
.collect(Collectors.toList());
|
||||
Set<String> allEmails = new HashSet<>();
|
||||
Set<String> allPhoneNumber = new HashSet<>();
|
||||
Map<String, String> allThirdParty = new HashMap<>();
|
||||
for (BulkImportUser user : users) {
|
||||
for (LoginMethod loginMethod : user.loginMethods) {
|
||||
if (loginMethod.email != null) {
|
||||
allEmails.add(loginMethod.email);
|
||||
}
|
||||
if (loginMethod.phoneNumber != null) {
|
||||
allPhoneNumber.add(loginMethod.phoneNumber);
|
||||
}
|
||||
if (loginMethod.thirdPartyId != null && loginMethod.thirdPartyUserId != null) {
|
||||
allThirdParty.put(loginMethod.thirdPartyUserId, loginMethod.thirdPartyId);
|
||||
}
|
||||
|
||||
}
|
||||
List<BulkImportUser> usersForAccountLinking = filterUsersInNeedOfAccountLinking(users);
|
||||
|
||||
if(usersForAccountLinking.isEmpty()){
|
||||
return;
|
||||
}
|
||||
|
||||
AuthRecipe.CreatePrimaryUsersResultHolder resultHolder;
|
||||
try {
|
||||
AuthRecipe.createPrimaryUsers(main, appIdentifier, storage, userIds, new ArrayList<>(allEmails),
|
||||
new ArrayList<>(allPhoneNumber), allThirdParty);
|
||||
resultHolder = AuthRecipe.createPrimaryUsersForBulkImport(main, appIdentifier, storage, usersForAccountLinking);
|
||||
} catch (StorageQueryException e) {
|
||||
if(e.getCause() instanceof BulkImportBatchInsertException){
|
||||
Map<String, Exception> errorsByPosition = ((BulkImportBatchInsertException) e.getCause()).exceptionByUserId;
|
||||
|
|
@ -521,27 +521,34 @@ public class BulkImport {
|
|||
} catch (FeatureNotEnabledException e) {
|
||||
throw new StorageTransactionLogicException(new Exception("E019: " + e.getMessage()));
|
||||
}
|
||||
if(resultHolder != null && resultHolder.usersWithSameExtraData != null){
|
||||
linkAccountsForMultipleUser(main, appIdentifier, storage, usersForAccountLinking, resultHolder.usersWithSameExtraData);
|
||||
}
|
||||
}
|
||||
|
||||
linkAccountsForMultipleUser(main, appIdentifier, storage, users, new ArrayList<>(allEmails),
|
||||
new ArrayList<>(allPhoneNumber), allThirdParty);
|
||||
private static List<BulkImportUser> filterUsersInNeedOfAccountLinking(List<BulkImportUser> allUsers) {
|
||||
if (allUsers == null || allUsers.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return allUsers.stream().filter(bulkImportUser -> bulkImportUser.loginMethods.stream()
|
||||
.anyMatch(loginMethod -> loginMethod.isPrimary) || bulkImportUser.loginMethods.size() > 1)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static void linkAccountsForMultipleUser(Main main, AppIdentifier appIdentifier, Storage storage,
|
||||
List<BulkImportUser> users,
|
||||
List<String> allDistinctEmails,
|
||||
List<String> allDistinctPhones,
|
||||
Map<String, String> thirdpartyUserIdsToThirdpartyIds)
|
||||
List<BulkImportUser> users, List<AuthRecipeUserInfo> allUsersWithSameExtraData)
|
||||
throws StorageTransactionLogicException {
|
||||
Map<String, String> recipeUserIdByPrimaryUserId = collectRecipeIdsToPrimaryIds(users);
|
||||
try {
|
||||
AuthRecipe.linkMultipleAccounts(main, appIdentifier, storage, recipeUserIdByPrimaryUserId,
|
||||
allDistinctEmails, allDistinctPhones, thirdpartyUserIdsToThirdpartyIds);
|
||||
AuthRecipe.linkMultipleAccountsForBulkImport(main, appIdentifier, storage,
|
||||
users, allUsersWithSameExtraData);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new StorageTransactionLogicException(new Exception("E023: " + e.getMessage()));
|
||||
} catch (FeatureNotEnabledException e) {
|
||||
throw new StorageTransactionLogicException(new Exception("E024: " + e.getMessage()));
|
||||
} catch (StorageQueryException e) {
|
||||
if (e.getCause() instanceof BulkImportBatchInsertException) {
|
||||
Map<String, String> recipeUserIdByPrimaryUserId = BulkImportUserUtils.collectRecipeIdsToPrimaryIds(users);
|
||||
|
||||
Map<String, Exception> errorByPosition = ((BulkImportBatchInsertException) e.getCause()).exceptionByUserId;
|
||||
for (String userId : errorByPosition.keySet()) {
|
||||
Exception currentException = errorByPosition.get(userId);
|
||||
|
|
@ -575,37 +582,23 @@ public class BulkImport {
|
|||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> collectRecipeIdsToPrimaryIds(List<BulkImportUser> users) {
|
||||
Map<String, String> recipeUserIdByPrimaryUserId = new HashMap<>();
|
||||
for(BulkImportUser user: users){
|
||||
LoginMethod primaryLM = getPrimaryLoginMethod(user);
|
||||
for (LoginMethod lm : user.loginMethods) {
|
||||
if (lm.getSuperTokenOrExternalUserId().equals(primaryLM.getSuperTokenOrExternalUserId())) {
|
||||
continue;
|
||||
}
|
||||
recipeUserIdByPrimaryUserId.put(lm.getSuperTokenOrExternalUserId(),
|
||||
primaryLM.getSuperTokenOrExternalUserId());
|
||||
}
|
||||
}
|
||||
return recipeUserIdByPrimaryUserId;
|
||||
}
|
||||
|
||||
public static void createMultipleUserIdMapping(AppIdentifier appIdentifier,
|
||||
List<BulkImportUser> users, Storage[] storages) throws StorageTransactionLogicException {
|
||||
Map<String, String> superTokensUserIdToExternalUserId = new HashMap<>();
|
||||
for(BulkImportUser user: users) {
|
||||
if(user.externalUserId != null) {
|
||||
LoginMethod primaryLoginMethod = getPrimaryLoginMethod(user);
|
||||
LoginMethod primaryLoginMethod = BulkImportUserUtils.getPrimaryLoginMethod(user);
|
||||
superTokensUserIdToExternalUserId.put(primaryLoginMethod.superTokensUserId, user.externalUserId);
|
||||
primaryLoginMethod.externalUserId = user.externalUserId;
|
||||
}
|
||||
}
|
||||
try {
|
||||
List<UserIdMapping.UserIdBulkMappingResult> mappingResults = UserIdMapping.createMultipleUserIdMappings(
|
||||
appIdentifier, storages,
|
||||
superTokensUserIdToExternalUserId,
|
||||
false, true);
|
||||
|
||||
if(!superTokensUserIdToExternalUserId.isEmpty()) {
|
||||
List<UserIdMapping.UserIdBulkMappingResult> mappingResults = UserIdMapping.createMultipleUserIdMappings(
|
||||
appIdentifier, storages,
|
||||
superTokensUserIdToExternalUserId,
|
||||
false, true);
|
||||
}
|
||||
} catch (StorageQueryException e) {
|
||||
if(e.getCause() instanceof BulkImportBatchInsertException) {
|
||||
Map<String, Exception> errorsByPosition = ((BulkImportBatchInsertException) e.getCause()).exceptionByUserId;
|
||||
|
|
@ -637,12 +630,14 @@ public class BulkImport {
|
|||
Map<String, JsonObject> usersMetadata = new HashMap<>();
|
||||
for(BulkImportUser user: users) {
|
||||
if (user.userMetadata != null) {
|
||||
usersMetadata.put(getPrimaryLoginMethod(user).getSuperTokenOrExternalUserId(), user.userMetadata);
|
||||
usersMetadata.put(BulkImportUserUtils.getPrimaryLoginMethod(user).getSuperTokenOrExternalUserId(), user.userMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
UserMetadata.updateMultipleUsersMetadata(appIdentifier, storage, usersMetadata);
|
||||
if(!usersMetadata.isEmpty()) {
|
||||
UserMetadata.updateMultipleUsersMetadata(appIdentifier, storage, usersMetadata);
|
||||
}
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new StorageTransactionLogicException(new Exception("E040: " + e.getMessage()));
|
||||
} catch (StorageQueryException e) {
|
||||
|
|
@ -652,27 +647,7 @@ public class BulkImport {
|
|||
|
||||
public static void createMultipleUserRoles(Main main, AppIdentifier appIdentifier, Storage storage,
|
||||
List<BulkImportUser> users) throws StorageTransactionLogicException {
|
||||
Map<TenantIdentifier, Map<String, List<String>>> rolesToUserByTenant = new HashMap<>();
|
||||
for (BulkImportUser user : users) {
|
||||
|
||||
if (user.userRoles != null) {
|
||||
for (UserRole userRole : user.userRoles) {
|
||||
for (String tenantId : userRole.tenantIds) {
|
||||
TenantIdentifier tenantIdentifier = new TenantIdentifier(
|
||||
appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(),
|
||||
tenantId);
|
||||
if(!rolesToUserByTenant.containsKey(tenantIdentifier)){
|
||||
|
||||
rolesToUserByTenant.put(tenantIdentifier, new HashMap<>());
|
||||
}
|
||||
if(!rolesToUserByTenant.get(tenantIdentifier).containsKey(user.externalUserId)){
|
||||
rolesToUserByTenant.get(tenantIdentifier).put(user.externalUserId, new ArrayList<>());
|
||||
}
|
||||
rolesToUserByTenant.get(tenantIdentifier).get(user.externalUserId).add(userRole.role);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<TenantIdentifier, Map<String, List<String>>> rolesToUserByTenant = gatherRolesForUsersByTenant(appIdentifier, users);
|
||||
try {
|
||||
if(!rolesToUserByTenant.isEmpty()){
|
||||
UserRoles.addMultipleRolesToMultipleUsers(main, appIdentifier, storage, rolesToUserByTenant);
|
||||
|
|
@ -698,34 +673,94 @@ public class BulkImport {
|
|||
|
||||
}
|
||||
|
||||
private static Map<TenantIdentifier, Map<String, List<String>>> gatherRolesForUsersByTenant(AppIdentifier appIdentifier, List<BulkImportUser> users) {
|
||||
Map<TenantIdentifier, Map<String, List<String>>> rolesToUserByTenant = new HashMap<>();
|
||||
for (BulkImportUser user : users) {
|
||||
if (user.userRoles != null) {
|
||||
for (UserRole userRole : user.userRoles) {
|
||||
for (String tenantId : userRole.tenantIds) {
|
||||
TenantIdentifier tenantIdentifier = new TenantIdentifier(
|
||||
appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(),
|
||||
tenantId);
|
||||
if(!rolesToUserByTenant.containsKey(tenantIdentifier)){
|
||||
|
||||
rolesToUserByTenant.put(tenantIdentifier, new HashMap<>());
|
||||
}
|
||||
String userIdToUse = user.externalUserId != null ?
|
||||
user.externalUserId : user.id;
|
||||
if(!rolesToUserByTenant.get(tenantIdentifier).containsKey(userIdToUse)){
|
||||
rolesToUserByTenant.get(tenantIdentifier).put(userIdToUse, new ArrayList<>());
|
||||
}
|
||||
rolesToUserByTenant.get(tenantIdentifier).get(userIdToUse).add(userRole.role);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rolesToUserByTenant;
|
||||
}
|
||||
|
||||
public static void verifyMultipleEmailForAllLoginMethods(AppIdentifier appIdentifier, Storage storage,
|
||||
List<BulkImportUser> users)
|
||||
throws StorageTransactionLogicException {
|
||||
Map<String, String> emailToUserId = new HashMap<>();
|
||||
for (BulkImportUser user : users) {
|
||||
for (LoginMethod lm : user.loginMethods) {
|
||||
emailToUserId.put(lm.getSuperTokenOrExternalUserId(), lm.email);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> emailToUserId = collectVerifiedEmailAddressesByUserIds(users);
|
||||
try {
|
||||
verifyCollectedEmailAddressesForUsers(appIdentifier, storage, emailToUserId);
|
||||
} catch (StorageQueryException | StorageTransactionLogicException e) {
|
||||
if (e.getCause() instanceof BulkImportBatchInsertException) {
|
||||
Map<String, Exception> errorsByPosition =
|
||||
((BulkImportBatchInsertException) e.getCause()).exceptionByUserId;
|
||||
for (String userid : errorsByPosition.keySet()) {
|
||||
Exception exception = errorsByPosition.get(userid);
|
||||
if (exception instanceof DuplicateEmailException) {
|
||||
String message =
|
||||
"E043: Email " + errorsByPosition.get(userid) + " is already verified for the user";
|
||||
errorsByPosition.put(userid, new Exception(message));
|
||||
} else if (exception instanceof NullPointerException) {
|
||||
String message = "E044: null email address was found for the userId " + userid +
|
||||
" while verifying the email";
|
||||
errorsByPosition.put(userid, new Exception(message));
|
||||
}
|
||||
}
|
||||
throw new StorageTransactionLogicException(
|
||||
new BulkImportBatchInsertException("translated", errorsByPosition));
|
||||
}
|
||||
throw new StorageTransactionLogicException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyCollectedEmailAddressesForUsers(AppIdentifier appIdentifier, Storage storage,
|
||||
Map<String, String> emailToUserId)
|
||||
throws StorageQueryException, StorageTransactionLogicException {
|
||||
if(!emailToUserId.isEmpty()) {
|
||||
EmailVerificationSQLStorage emailVerificationSQLStorage = StorageUtils
|
||||
.getEmailVerificationStorage(storage);
|
||||
emailVerificationSQLStorage.startTransaction(con -> {
|
||||
emailVerificationSQLStorage
|
||||
.updateMultipleIsEmailVerified_Transaction(appIdentifier, con,
|
||||
emailToUserId, true);
|
||||
emailToUserId, true); //only the verified email addresses are expected to be in the map
|
||||
|
||||
emailVerificationSQLStorage.commitTransaction(con);
|
||||
return null;
|
||||
});
|
||||
|
||||
} catch (StorageQueryException e) {
|
||||
throw new StorageTransactionLogicException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, String> collectVerifiedEmailAddressesByUserIds(List<BulkImportUser> users) {
|
||||
Map<String, String> emailToUserId = new LinkedHashMap<>();
|
||||
for (BulkImportUser user : users) {
|
||||
for (LoginMethod lm : user.loginMethods) {
|
||||
//we skip passwordless` 'null' email addresses
|
||||
if (lm.isVerified && !(lm.recipeId.equals("passwordless") && lm.email == null)) {
|
||||
//collect the verified email addresses for the userId
|
||||
emailToUserId.put(lm.getSuperTokenOrExternalUserId(), lm.email);
|
||||
}
|
||||
}
|
||||
}
|
||||
return emailToUserId;
|
||||
}
|
||||
|
||||
public static void createMultipleTotpDevices(Main main, AppIdentifier appIdentifier,
|
||||
Storage storage, List<BulkImportUser> users)
|
||||
throws StorageTransactionLogicException {
|
||||
|
|
@ -733,7 +768,7 @@ public class BulkImport {
|
|||
for (BulkImportUser user : users) {
|
||||
if (user.totpDevices != null) {
|
||||
for(TotpDevice device : user.totpDevices){
|
||||
TOTPDevice totpDevice = new TOTPDevice(getPrimaryLoginMethod(user).getSuperTokenOrExternalUserId(),
|
||||
TOTPDevice totpDevice = new TOTPDevice(BulkImportUserUtils.getPrimaryLoginMethod(user).getSuperTokenOrExternalUserId(),
|
||||
device.deviceName, device.secretKey, device.period, device.skew, true,
|
||||
System.currentTimeMillis());
|
||||
devices.add(totpDevice);
|
||||
|
|
@ -751,21 +786,6 @@ public class BulkImport {
|
|||
}
|
||||
}
|
||||
|
||||
// Returns the primary loginMethod of the user. If no loginMethod is marked as
|
||||
// primary, then the oldest loginMethod is returned.
|
||||
public static BulkImportUser.LoginMethod getPrimaryLoginMethod(BulkImportUser user) {
|
||||
BulkImportUser.LoginMethod oldestLM = user.loginMethods.get(0);
|
||||
for (BulkImportUser.LoginMethod lm : user.loginMethods) {
|
||||
if (lm.isPrimary) {
|
||||
return lm;
|
||||
}
|
||||
|
||||
if (lm.timeJoinedInMSSinceEpoch < oldestLM.timeJoinedInMSSinceEpoch) {
|
||||
oldestLM = lm;
|
||||
}
|
||||
}
|
||||
return oldestLM;
|
||||
}
|
||||
|
||||
private static synchronized Storage getBulkImportProxyStorage(Main main, TenantIdentifier tenantIdentifier)
|
||||
throws InvalidConfigException, IOException, TenantOrAppNotFoundException, DbInitException {
|
||||
|
|
|
|||
|
|
@ -16,16 +16,9 @@
|
|||
|
||||
package io.supertokens.bulkimport;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import io.supertokens.Main;
|
||||
import io.supertokens.bulkimport.exceptions.InvalidBulkImportDataException;
|
||||
import io.supertokens.config.CoreConfig;
|
||||
|
|
@ -37,16 +30,18 @@ import io.supertokens.multitenancy.Multitenancy;
|
|||
import io.supertokens.pluginInterface.Storage;
|
||||
import io.supertokens.pluginInterface.bulkimport.BulkImportUser;
|
||||
import io.supertokens.pluginInterface.bulkimport.BulkImportUser.LoginMethod;
|
||||
import io.supertokens.pluginInterface.bulkimport.BulkImportUser.UserRole;
|
||||
import io.supertokens.pluginInterface.bulkimport.BulkImportUser.TotpDevice;
|
||||
import io.supertokens.pluginInterface.bulkimport.BulkImportUser.UserRole;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
|
||||
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
|
||||
import io.supertokens.pluginInterface.multitenancy.TenantConfig;
|
||||
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
||||
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
|
||||
import io.supertokens.storageLayer.StorageLayer;
|
||||
import io.supertokens.utils.Utils;
|
||||
import io.supertokens.utils.JsonValidatorUtils.ValueType;
|
||||
import io.supertokens.utils.Utils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static io.supertokens.utils.JsonValidatorUtils.parseAndValidateFieldType;
|
||||
import static io.supertokens.utils.JsonValidatorUtils.validateJsonFieldType;
|
||||
|
|
@ -60,8 +55,7 @@ public class BulkImportUserUtils {
|
|||
this.allExternalUserIds = new HashSet<>();
|
||||
}
|
||||
|
||||
public BulkImportUser createBulkImportUserFromJSON(Main main, AppIdentifier appIdentifier, JsonObject userData,
|
||||
String id)
|
||||
public BulkImportUser createBulkImportUserFromJSON(Main main, AppIdentifier appIdentifier, JsonObject userData, IDMode idMode)
|
||||
throws InvalidBulkImportDataException, StorageQueryException, TenantOrAppNotFoundException {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
|
|
@ -72,7 +66,7 @@ public class BulkImportUserUtils {
|
|||
JsonObject.class, errors, ".");
|
||||
List<UserRole> userRoles = getParsedUserRoles(main, appIdentifier, userData, errors);
|
||||
List<TotpDevice> totpDevices = getParsedTotpDevices(main, appIdentifier, userData, errors);
|
||||
List<LoginMethod> loginMethods = getParsedLoginMethods(main, appIdentifier, userData, errors);
|
||||
List<LoginMethod> loginMethods = getParsedLoginMethods(main, appIdentifier, userData, errors, idMode);
|
||||
|
||||
externalUserId = validateAndNormaliseExternalUserId(externalUserId, errors);
|
||||
|
||||
|
|
@ -81,6 +75,7 @@ public class BulkImportUserUtils {
|
|||
if (!errors.isEmpty()) {
|
||||
throw new InvalidBulkImportDataException(errors);
|
||||
}
|
||||
String id = getPrimaryLoginMethod(loginMethods).superTokensUserId;
|
||||
return new BulkImportUser(id, externalUserId, userMetadata, userRoles, totpDevices, loginMethods);
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +150,7 @@ public class BulkImportUserUtils {
|
|||
}
|
||||
|
||||
private List<LoginMethod> getParsedLoginMethods(Main main, AppIdentifier appIdentifier, JsonObject userData,
|
||||
List<String> errors)
|
||||
List<String> errors, IDMode idMode)
|
||||
throws StorageQueryException, TenantOrAppNotFoundException {
|
||||
JsonArray jsonLoginMethods = parseAndValidateFieldType(userData, "loginMethods", ValueType.ARRAY_OF_OBJECT,
|
||||
true, JsonArray.class, errors, ".");
|
||||
|
|
@ -193,6 +188,7 @@ public class BulkImportUserUtils {
|
|||
Long timeJoined = parseAndValidateFieldType(jsonLoginMethodObj, "timeJoinedInMSSinceEpoch", ValueType.LONG,
|
||||
false, Long.class, errors, " for a loginMethod");
|
||||
|
||||
|
||||
recipeId = validateAndNormaliseRecipeId(recipeId, errors);
|
||||
List<String> normalisedTenantIds = validateAndNormaliseTenantIds(main, appIdentifier, tenantIds, errors,
|
||||
" for " + recipeId + " recipe.");
|
||||
|
|
@ -201,6 +197,12 @@ public class BulkImportUserUtils {
|
|||
|
||||
long timeJoinedInMSSinceEpoch = validateAndNormaliseTimeJoined(timeJoined, errors);
|
||||
|
||||
String supertokensUserId = switch (idMode) {
|
||||
case READ_STORED -> parseAndValidateFieldType(jsonLoginMethodObj, "superTokensUserId", ValueType.STRING,
|
||||
true, String.class, errors, " for a loginMethod");
|
||||
case GENERATE -> Utils.getUUID();
|
||||
};
|
||||
|
||||
if ("emailpassword".equals(recipeId)) {
|
||||
String email = parseAndValidateFieldType(jsonLoginMethodObj, "email", ValueType.STRING, true,
|
||||
String.class, errors, " for an emailpassword recipe.");
|
||||
|
|
@ -224,7 +226,8 @@ public class BulkImportUserUtils {
|
|||
passwordHash, errors);
|
||||
|
||||
loginMethods.add(new LoginMethod(normalisedTenantIds, recipeId, isVerified, isPrimary,
|
||||
timeJoinedInMSSinceEpoch, email, passwordHash, hashingAlgorithm, null, null, null, null));
|
||||
timeJoinedInMSSinceEpoch, email, passwordHash, hashingAlgorithm, plainTextPassword,
|
||||
null, null, null, supertokensUserId));
|
||||
} else if ("thirdparty".equals(recipeId)) {
|
||||
String email = parseAndValidateFieldType(jsonLoginMethodObj, "email", ValueType.STRING, true,
|
||||
String.class, errors, " for a thirdparty recipe.");
|
||||
|
|
@ -238,7 +241,8 @@ public class BulkImportUserUtils {
|
|||
thirdPartyUserId = validateAndNormaliseThirdPartyUserId(thirdPartyUserId, errors);
|
||||
|
||||
loginMethods.add(new LoginMethod(normalisedTenantIds, recipeId, isVerified, isPrimary,
|
||||
timeJoinedInMSSinceEpoch, email, null, null, null, thirdPartyId, thirdPartyUserId, null));
|
||||
timeJoinedInMSSinceEpoch, email, null, null, null,
|
||||
thirdPartyId, thirdPartyUserId, null, supertokensUserId));
|
||||
} else if ("passwordless".equals(recipeId)) {
|
||||
String email = parseAndValidateFieldType(jsonLoginMethodObj, "email", ValueType.STRING, false,
|
||||
String.class, errors, " for a passwordless recipe.");
|
||||
|
|
@ -253,7 +257,8 @@ public class BulkImportUserUtils {
|
|||
}
|
||||
|
||||
loginMethods.add(new LoginMethod(normalisedTenantIds, recipeId, isVerified, isPrimary,
|
||||
timeJoinedInMSSinceEpoch, email, null, null, null, null, null, phoneNumber));
|
||||
timeJoinedInMSSinceEpoch, email, null, null, null,
|
||||
null, null, phoneNumber, supertokensUserId));
|
||||
}
|
||||
}
|
||||
return loginMethods;
|
||||
|
|
@ -576,4 +581,74 @@ public class BulkImportUserUtils {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static BulkImportUser.LoginMethod getPrimaryLoginMethod(BulkImportUser user) {
|
||||
return getPrimaryLoginMethod(user.loginMethods);
|
||||
}
|
||||
|
||||
// Returns the primary loginMethod of the user. If no loginMethod is marked as
|
||||
// primary, then the oldest loginMethod is returned.
|
||||
public static BulkImportUser.LoginMethod getPrimaryLoginMethod(List<LoginMethod> loginMethods) {
|
||||
BulkImportUser.LoginMethod oldestLM = loginMethods.get(0);
|
||||
for (BulkImportUser.LoginMethod lm : loginMethods) {
|
||||
if (lm.isPrimary) {
|
||||
return lm;
|
||||
}
|
||||
|
||||
if (lm.timeJoinedInMSSinceEpoch < oldestLM.timeJoinedInMSSinceEpoch) {
|
||||
oldestLM = lm;
|
||||
}
|
||||
}
|
||||
return oldestLM;
|
||||
}
|
||||
|
||||
public enum IDMode {
|
||||
GENERATE,
|
||||
READ_STORED;
|
||||
}
|
||||
|
||||
// Returns a map of recipe user ids -> primary user ids
|
||||
public static Map<String, String> collectRecipeIdsToPrimaryIds(List<BulkImportUser> users) {
|
||||
Map<String, String> recipeUserIdByPrimaryUserId = new HashMap<>();
|
||||
if(users == null){
|
||||
return recipeUserIdByPrimaryUserId;
|
||||
}
|
||||
for(BulkImportUser user: users){
|
||||
LoginMethod primaryLM = BulkImportUserUtils.getPrimaryLoginMethod(user);
|
||||
for (LoginMethod lm : user.loginMethods) {
|
||||
if (lm.getSuperTokenOrExternalUserId().equals(primaryLM.getSuperTokenOrExternalUserId())) {
|
||||
continue;
|
||||
}
|
||||
recipeUserIdByPrimaryUserId.put(lm.getSuperTokenOrExternalUserId(),
|
||||
primaryLM.getSuperTokenOrExternalUserId());
|
||||
}
|
||||
}
|
||||
return recipeUserIdByPrimaryUserId;
|
||||
}
|
||||
|
||||
public static LoginMethod findLoginMethodByRecipeUserId(List<BulkImportUser> users, String recipeUserId) {
|
||||
if(users == null || users.isEmpty() || recipeUserId == null){
|
||||
return null;
|
||||
}
|
||||
for(BulkImportUser user: users) {
|
||||
for (LoginMethod loginMethod : user.loginMethods) {
|
||||
if (recipeUserId.equals(loginMethod.superTokensUserId)) {
|
||||
return loginMethod;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static BulkImportUser findUserByPrimaryId(List<BulkImportUser> users, String primaryUserId) {
|
||||
if(users == null || users.isEmpty() || primaryUserId == null){
|
||||
return null;
|
||||
}
|
||||
for(BulkImportUser user: users) {
|
||||
if(primaryUserId.equals(user.primaryUserId)){
|
||||
return user;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ public class Config extends ResourceDistributor.SingletonResource {
|
|||
final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
|
||||
Object configObj = mapper.readValue(new File(configFilePath), Object.class);
|
||||
JsonObject jsonConfig = new GsonBuilder().serializeNulls().create().toJsonTree(configObj).getAsJsonObject();
|
||||
CoreConfig.updateConfigJsonFromEnv(jsonConfig);
|
||||
StorageLayer.updateConfigJsonFromEnv(main, jsonConfig);
|
||||
CoreConfig config = ConfigMapper.mapConfig(jsonConfig, CoreConfig.class);
|
||||
config.normalizeAndValidate(main, true);
|
||||
this.core = config;
|
||||
|
|
@ -91,12 +93,20 @@ public class Config extends ResourceDistributor.SingletonResource {
|
|||
// omit them from the output json.
|
||||
ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
|
||||
Object obj = yamlReader.readValue(new File(getConfigFilePath(main)), Object.class);
|
||||
return new GsonBuilder().serializeNulls().create().toJsonTree(obj).getAsJsonObject();
|
||||
JsonObject configJson = new GsonBuilder().serializeNulls().create().toJsonTree(obj).getAsJsonObject();
|
||||
CoreConfig.updateConfigJsonFromEnv(configJson);
|
||||
StorageLayer.updateConfigJsonFromEnv(main, configJson);
|
||||
return configJson;
|
||||
}
|
||||
|
||||
private static String getConfigFilePath(Main main) {
|
||||
String configFile = "config.yaml";
|
||||
if (Main.isTesting) {
|
||||
String workerId = System.getProperty("org.gradle.test.worker", "");
|
||||
configFile = "config" + workerId + ".yaml";
|
||||
}
|
||||
return CLIOptions.get(main).getConfigFilePath() == null
|
||||
? CLIOptions.get(main).getInstallationPath() + "config.yaml"
|
||||
? CLIOptions.get(main).getInstallationPath() + configFile
|
||||
: CLIOptions.get(main).getConfigFilePath();
|
||||
}
|
||||
|
||||
|
|
@ -305,7 +315,7 @@ public class Config extends ResourceDistributor.SingletonResource {
|
|||
@TestOnly
|
||||
public static CoreConfig getConfig(Main main) {
|
||||
try {
|
||||
return getConfig(new TenantIdentifier(null, null, null), main);
|
||||
return getConfig(ResourceDistributor.getAppForTesting(), main);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@ public class CoreConfig {
|
|||
"oauth_provider_public_service_url",
|
||||
"oauth_provider_admin_service_url",
|
||||
"oauth_provider_consent_login_base_url",
|
||||
"oauth_provider_url_configured_in_oauth_provider"
|
||||
"oauth_provider_url_configured_in_oauth_provider",
|
||||
"saml_legacy_acs_url"
|
||||
};
|
||||
|
||||
@IgnoreForAnnotationCheck
|
||||
|
|
@ -75,11 +76,13 @@ public class CoreConfig {
|
|||
@ConfigDescription("The version of the core config.")
|
||||
private int core_config_version = -1;
|
||||
|
||||
@EnvName("ACCESS_TOKEN_VALIDITY")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@ConfigDescription("Time in seconds for how long an access token is valid for. [Default: 3600 (1 hour)]")
|
||||
private long access_token_validity = 3600; // in seconds
|
||||
|
||||
@EnvName("ACCESS_TOKEN_BLACKLISTING")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -88,17 +91,20 @@ public class CoreConfig {
|
|||
"call that requires authentication. (Default: false)")
|
||||
private boolean access_token_blacklisting = false;
|
||||
|
||||
@EnvName("REFRESH_TOKEN_VALIDITY")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@ConfigDescription("Time in mins for how long a refresh token is valid for. [Default: 60 * 2400 (100 days)]")
|
||||
private double refresh_token_validity = 60 * 2400; // in mins
|
||||
|
||||
@EnvName("PASSWORD_RESET_TOKEN_LIFETIME")
|
||||
@IgnoreForAnnotationCheck
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
"Time in milliseconds for how long a password reset token / link is valid for. [Default: 3600000 (1 hour)]")
|
||||
private long password_reset_token_lifetime = 3600000; // in MS
|
||||
|
||||
@EnvName("EMAIL_VERIFICATION_TOKEN_LIFETIME")
|
||||
@IgnoreForAnnotationCheck
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -106,23 +112,27 @@ public class CoreConfig {
|
|||
" 1000 (1 day)]")
|
||||
private long email_verification_token_lifetime = 24 * 3600 * 1000; // in MS
|
||||
|
||||
@EnvName("PASSWORDLESS_MAX_CODE_INPUT_ATTEMPTS")
|
||||
@IgnoreForAnnotationCheck
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
"The maximum number of code input attempts per login before the user needs to restart. (Default: 5)")
|
||||
private int passwordless_max_code_input_attempts = 5;
|
||||
|
||||
@EnvName("PASSWORDLESS_CODE_LIFETIME")
|
||||
@IgnoreForAnnotationCheck
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
"Time in milliseconds for how long a passwordless code is valid for. [Default: 900000 (15 mins)]")
|
||||
private long passwordless_code_lifetime = 900000; // in MS
|
||||
|
||||
@EnvName("TOTP_MAX_ATTEMPTS")
|
||||
@IgnoreForAnnotationCheck
|
||||
@JsonProperty
|
||||
@ConfigDescription("The maximum number of invalid TOTP attempts that will trigger rate limiting. (Default: 5)")
|
||||
private int totp_max_attempts = 5;
|
||||
|
||||
@EnvName("TOTP_RATE_LIMIT_COOLDOWN_SEC")
|
||||
@IgnoreForAnnotationCheck
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -133,6 +143,7 @@ public class CoreConfig {
|
|||
@IgnoreForAnnotationCheck
|
||||
private final String logDefault = "asdkfahbdfk3kjHS";
|
||||
|
||||
@EnvName("INFO_LOG_PATH")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -141,6 +152,7 @@ public class CoreConfig {
|
|||
"directory/logs/info.log)")
|
||||
private String info_log_path = logDefault;
|
||||
|
||||
@EnvName("ERROR_LOG_PATH")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -149,6 +161,7 @@ public class CoreConfig {
|
|||
"directory/logs/error.log)")
|
||||
private String error_log_path = logDefault;
|
||||
|
||||
@EnvName("ACCESS_TOKEN_SIGNING_KEY_DYNAMIC")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -156,17 +169,20 @@ public class CoreConfig {
|
|||
" be signed using a static signing key. (Default: true)")
|
||||
private boolean access_token_signing_key_dynamic = true;
|
||||
|
||||
@EnvName("ACCESS_TOKEN_DYNAMIC_SIGNING_KEY_UPDATE_INTERVAL")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty("access_token_dynamic_signing_key_update_interval")
|
||||
@JsonAlias({"access_token_dynamic_signing_key_update_interval", "access_token_signing_key_update_interval"})
|
||||
@ConfigDescription("Time in hours for how frequently the dynamic signing key will change. [Default: 168 (1 week)]")
|
||||
private double access_token_dynamic_signing_key_update_interval = 168; // in hours
|
||||
|
||||
@EnvName("SUPERTOKENS_PORT")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription("The port at which SuperTokens service runs. (Default: 3567)")
|
||||
private int port = 3567;
|
||||
|
||||
@EnvName("SUPERTOKENS_HOST")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -174,11 +190,13 @@ public class CoreConfig {
|
|||
" address associated with your machine. (Default: localhost)")
|
||||
private String host = "localhost";
|
||||
|
||||
@EnvName("MAX_SERVER_POOL_SIZE")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription("Sets the max thread pool size for incoming http server requests. (Default: 10)")
|
||||
private int max_server_pool_size = 10;
|
||||
|
||||
@EnvName("API_KEYS")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@HideFromDashboard
|
||||
|
|
@ -188,6 +206,7 @@ public class CoreConfig {
|
|||
"length of 20 chars. (Default: null)")
|
||||
private String api_keys = null;
|
||||
|
||||
@EnvName("DISABLE_TELEMETRY")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -195,27 +214,32 @@ public class CoreConfig {
|
|||
"(Default: false)")
|
||||
private boolean disable_telemetry = false;
|
||||
|
||||
@EnvName("PASSWORD_HASHING_ALG")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@ConfigDescription("The password hashing algorithm to use. Values are \"ARGON2\" | \"BCRYPT\". (Default: BCRYPT)")
|
||||
@EnumProperty({"ARGON2", "BCRYPT"})
|
||||
private String password_hashing_alg = "BCRYPT";
|
||||
|
||||
@EnvName("ARGON2_ITERATIONS")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription("Number of iterations for argon2 password hashing. (Default: 1)")
|
||||
private int argon2_iterations = 1;
|
||||
|
||||
@EnvName("ARGON2_MEMORY_KB")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription("Amount of memory in kb for argon2 password hashing. [Default: 87795 (85 mb)]")
|
||||
private int argon2_memory_kb = 87795; // 85 mb
|
||||
|
||||
@EnvName("ARGON2_PARALLELISM")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription("Amount of parallelism for argon2 password hashing. (Default: 2)")
|
||||
private int argon2_parallelism = 2;
|
||||
|
||||
@EnvName("ARGON2_HASHING_POOL_SIZE")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -223,6 +247,7 @@ public class CoreConfig {
|
|||
"(Default: 1)")
|
||||
private int argon2_hashing_pool_size = 1;
|
||||
|
||||
@EnvName("FIREBASE_PASSWORD_HASHING_POOL_SIZE")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -230,6 +255,7 @@ public class CoreConfig {
|
|||
"(Default: 1)")
|
||||
private int firebase_password_hashing_pool_size = 1;
|
||||
|
||||
@EnvName("BCRYPT_LOG_ROUNDS")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription("Number of rounds to set for bcrypt password hashing. (Default: 11)")
|
||||
|
|
@ -245,13 +271,16 @@ public class CoreConfig {
|
|||
// # webserver_https_enabled:
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@IgnoreForAnnotationCheck
|
||||
private boolean webserver_https_enabled = false;
|
||||
|
||||
@EnvName("BASE_PATH")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription("Used to prepend a base path to all APIs when querying the core.")
|
||||
private String base_path = "";
|
||||
|
||||
@EnvName("LOG_LEVEL")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -260,11 +289,13 @@ public class CoreConfig {
|
|||
@EnumProperty({"DEBUG", "INFO", "WARN", "ERROR", "NONE"})
|
||||
private String log_level = "INFO";
|
||||
|
||||
@EnvName("FIREBASE_PASSWORD_HASHING_SIGNER_KEY")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@ConfigDescription("The signer key used for firebase scrypt password hashing. (Default: null)")
|
||||
private String firebase_password_hashing_signer_key = null;
|
||||
|
||||
@EnvName("IP_ALLOW_REGEX")
|
||||
@IgnoreForAnnotationCheck
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -272,6 +303,7 @@ public class CoreConfig {
|
|||
"127\\.\\d+\\.\\d+\\.\\d+|::1|0:0:0:0:0:0:0:1 to allow only localhost to query the core")
|
||||
private String ip_allow_regex = null;
|
||||
|
||||
@EnvName("IP_DENY_REGEX")
|
||||
@IgnoreForAnnotationCheck
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -279,6 +311,7 @@ public class CoreConfig {
|
|||
" address.")
|
||||
private String ip_deny_regex = null;
|
||||
|
||||
@EnvName("OAUTH_PROVIDER_PUBLIC_SERVICE_URL")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@HideFromDashboard
|
||||
|
|
@ -286,6 +319,7 @@ public class CoreConfig {
|
|||
"If specified, the core uses this URL to connect to the OAuth provider public service.")
|
||||
private String oauth_provider_public_service_url = null;
|
||||
|
||||
@EnvName("OAUTH_PROVIDER_ADMIN_SERVICE_URL")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@HideFromDashboard
|
||||
|
|
@ -293,6 +327,7 @@ public class CoreConfig {
|
|||
"If specified, the core uses this URL to connect to the OAuth provider admin service.")
|
||||
private String oauth_provider_admin_service_url = null;
|
||||
|
||||
@EnvName("OAUTH_PROVIDER_CONSENT_LOGIN_BASE_URL")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@HideFromDashboard
|
||||
|
|
@ -300,6 +335,7 @@ public class CoreConfig {
|
|||
"If specified, the core uses this URL to replace the default consent and login URLs to {apiDomain}.")
|
||||
private String oauth_provider_consent_login_base_url = null;
|
||||
|
||||
@EnvName("OAUTH_PROVIDER_URL_CONFIGURED_IN_OAUTH_PROVIDER")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@HideFromDashboard
|
||||
|
|
@ -307,12 +343,14 @@ public class CoreConfig {
|
|||
"If specified, the core uses this URL to parse responses from the oauth provider when the oauth provider's internal address differs from the known public provider address.")
|
||||
private String oauth_provider_url_configured_in_oauth_provider = null;
|
||||
|
||||
@EnvName("OAUTH_CLIENT_SECRET_ENCRYPTION_KEY")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@HideFromDashboard
|
||||
@ConfigDescription("The encryption key used for saving OAuth client secret on the database.")
|
||||
private String oauth_client_secret_encryption_key = null;
|
||||
|
||||
@EnvName("SUPERTOKENS_SAAS_SECRET")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -322,6 +360,7 @@ public class CoreConfig {
|
|||
"regular api_keys config.")
|
||||
private String supertokens_saas_secret = null;
|
||||
|
||||
@EnvName("SUPERTOKENS_MAX_CDI_VERSION")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@HideFromDashboard
|
||||
|
|
@ -331,6 +370,7 @@ public class CoreConfig {
|
|||
"null)")
|
||||
private String supertokens_max_cdi_version = null;
|
||||
|
||||
@EnvName("SUPERTOKENS_SAAS_LOAD_ONLY_CUD")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
|
|
@ -338,18 +378,73 @@ public class CoreConfig {
|
|||
"the database and block all other CUDs from being used from this instance.")
|
||||
private String supertokens_saas_load_only_cud = null;
|
||||
|
||||
@EnvName("SAML_LEGACY_ACS_URL")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@ConfigDescription("If specified, uses this URL as ACS URL for handling legacy SAML clients")
|
||||
@HideFromDashboard
|
||||
private String saml_legacy_acs_url = null;
|
||||
|
||||
@EnvName("SAML_SP_ENTITY_ID")
|
||||
@JsonProperty
|
||||
@IgnoreForAnnotationCheck
|
||||
@ConfigDescription("Service provider's entity ID")
|
||||
private String saml_sp_entity_id = null;
|
||||
|
||||
@EnvName("SAML_CLAIMS_VALIDITY")
|
||||
@JsonProperty
|
||||
@IgnoreForAnnotationCheck
|
||||
@ConfigDescription("Duration for which SAML claims will be valid before it is consumed")
|
||||
private long saml_claims_validity = 300000;
|
||||
|
||||
@EnvName("SAML_RELAY_STATE_VALIDITY")
|
||||
@JsonProperty
|
||||
@IgnoreForAnnotationCheck
|
||||
@ConfigDescription("Duration for which SAML relay state will be valid before it is consumed")
|
||||
private long saml_relay_state_validity = 300000;
|
||||
|
||||
@IgnoreForAnnotationCheck
|
||||
private Set<LOG_LEVEL> allowedLogLevels = null;
|
||||
|
||||
@IgnoreForAnnotationCheck
|
||||
private boolean isNormalizedAndValid = false;
|
||||
|
||||
@EnvName("BULK_MIGRATION_PARALLELISM")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@ConfigDescription("If specified, the supertokens core will use the specified number of threads to complete the " +
|
||||
"migration of users. (Default: number of available processor cores).")
|
||||
private int bulk_migration_parallelism = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
@EnvName("BULK_MIGRATION_BATCH_SIZE")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@ConfigDescription("If specified, the supertokens core will load the specified number of users for migrating in " +
|
||||
"one single batch. (Default: 8000)")
|
||||
private int bulk_migration_batch_size = 8000;
|
||||
|
||||
@EnvName("WEBAUTHN_RECOVER_ACCOUNT_TOKEN_LIFETIME")
|
||||
@NotConflictingInApp
|
||||
@JsonProperty
|
||||
@ConfigDescription("Time in milliseconds for how long a webauthn account recovery token is valid for. [Default: 3600000 (1 hour)]")
|
||||
private long webauthn_recover_account_token_lifetime = 3600000; // in MS;
|
||||
|
||||
@EnvName("OTEL_COLLECTOR_CONNECTION_URI")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
"The URL of the OpenTelemetry collector to which the core will send telemetry data. " +
|
||||
"This should be in the format http://<host>:<port> or https://<host>:<port>. (Default: " +
|
||||
"null)")
|
||||
private String otel_collector_connection_uri = null;
|
||||
|
||||
@EnvName("DEADLOCK_LOGGER_ENABLE")
|
||||
@ConfigYamlOnly
|
||||
@JsonProperty
|
||||
@ConfigDescription(
|
||||
"Enables or disables the deadlock logger. (Default: false)")
|
||||
private boolean deadlock_logger_enable = false;
|
||||
|
||||
@IgnoreForAnnotationCheck
|
||||
private static boolean disableOAuthValidationForTest = false;
|
||||
|
||||
|
|
@ -418,6 +513,10 @@ public class CoreConfig {
|
|||
return ip_deny_regex;
|
||||
}
|
||||
|
||||
public String getLogLevel() {
|
||||
return log_level;
|
||||
}
|
||||
|
||||
public Set<LOG_LEVEL> getLogLevels(Main main) {
|
||||
if (allowedLogLevels != null) {
|
||||
return allowedLogLevels;
|
||||
|
|
@ -589,12 +688,84 @@ public class CoreConfig {
|
|||
return bulk_migration_parallelism;
|
||||
}
|
||||
|
||||
public long getWebauthnRecoverAccountTokenLifetime() {
|
||||
return webauthn_recover_account_token_lifetime;
|
||||
}
|
||||
|
||||
public int getBulkMigrationBatchSize() {
|
||||
return bulk_migration_batch_size;
|
||||
}
|
||||
|
||||
public String getOtelCollectorConnectionURI() {
|
||||
return otel_collector_connection_uri;
|
||||
}
|
||||
|
||||
public boolean isDeadlockLoggerEnabled() {
|
||||
return deadlock_logger_enable;
|
||||
}
|
||||
|
||||
public String getSAMLLegacyACSURL() {
|
||||
return saml_legacy_acs_url;
|
||||
}
|
||||
|
||||
public String getSAMLSPEntityID() {
|
||||
return saml_sp_entity_id;
|
||||
}
|
||||
|
||||
public long getSAMLClaimsValidity() {
|
||||
return saml_claims_validity;
|
||||
}
|
||||
|
||||
public long getSAMLRelayStateValidity() {
|
||||
return saml_relay_state_validity;
|
||||
}
|
||||
|
||||
private String getConfigFileLocation(Main main) {
|
||||
return new File(CLIOptions.get(main).getConfigFilePath() == null
|
||||
? CLIOptions.get(main).getInstallationPath() + "config.yaml"
|
||||
: CLIOptions.get(main).getConfigFilePath()).getAbsolutePath();
|
||||
}
|
||||
|
||||
public static void updateConfigJsonFromEnv(JsonObject configJson) {
|
||||
Map<String, String> env = System.getenv();
|
||||
|
||||
for (Field field : CoreConfig.class.getDeclaredFields()) {
|
||||
if (field.isAnnotationPresent(EnvName.class)) {
|
||||
String envName = field.getAnnotation(EnvName.class).value();
|
||||
String stringValue = env.get(envName);
|
||||
|
||||
if (stringValue == null || stringValue.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stringValue.startsWith("\"") && stringValue.endsWith("\"")) {
|
||||
stringValue = stringValue.substring(1, stringValue.length() - 1);
|
||||
stringValue = stringValue
|
||||
.replace("\\n", "\n")
|
||||
.replace("\\t", "\t")
|
||||
.replace("\\r", "\r")
|
||||
.replace("\\\"", "\"")
|
||||
.replace("\\'", "'")
|
||||
.replace("\\\\", "\\");
|
||||
}
|
||||
|
||||
if (field.getType().equals(String.class)) {
|
||||
configJson.addProperty(field.getName(), stringValue);
|
||||
} else if (field.getType().equals(int.class)) {
|
||||
configJson.addProperty(field.getName(), Integer.parseInt(stringValue));
|
||||
} else if (field.getType().equals(long.class)) {
|
||||
configJson.addProperty(field.getName(), Long.parseLong(stringValue));
|
||||
} else if (field.getType().equals(boolean.class)) {
|
||||
configJson.addProperty(field.getName(), Boolean.parseBoolean(stringValue));
|
||||
} else if (field.getType().equals(float.class)) {
|
||||
configJson.addProperty(field.getName(), Float.parseFloat(stringValue));
|
||||
} else if (field.getType().equals(double.class)) {
|
||||
configJson.addProperty(field.getName(), Double.parseDouble(stringValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void normalizeAndValidate(Main main, boolean includeConfigFilePath) throws InvalidConfigException {
|
||||
if (isNormalizedAndValid) {
|
||||
return;
|
||||
|
|
@ -786,6 +957,14 @@ public class CoreConfig {
|
|||
throw new InvalidConfigException("Provided bulk_migration_parallelism must be >= 1");
|
||||
}
|
||||
|
||||
if (bulk_migration_batch_size < 1) {
|
||||
throw new InvalidConfigException("Provided bulk_migration_batch_size must be >= 1");
|
||||
}
|
||||
|
||||
if (webauthn_recover_account_token_lifetime <= 0) {
|
||||
throw new InvalidConfigException("Provided webauthn_recover_account_token_lifetime must be > 0");
|
||||
}
|
||||
|
||||
for (String fieldId : CoreConfig.getValidFields()) {
|
||||
try {
|
||||
Field field = CoreConfig.class.getDeclaredField(fieldId);
|
||||
|
|
@ -809,6 +988,10 @@ public class CoreConfig {
|
|||
}
|
||||
|
||||
// Normalize
|
||||
if (saml_sp_entity_id == null) {
|
||||
saml_sp_entity_id = "https://saml.supertokens.com";
|
||||
}
|
||||
|
||||
if (ip_allow_regex != null) {
|
||||
ip_allow_regex = ip_allow_regex.trim();
|
||||
if (ip_allow_regex.equals("")) {
|
||||
|
|
@ -946,6 +1129,24 @@ public class CoreConfig {
|
|||
}
|
||||
}
|
||||
|
||||
if (Main.isTesting) {
|
||||
if (oauth_provider_public_service_url == null) {
|
||||
oauth_provider_public_service_url = "http://localhost:" + System.getProperty("ST_OAUTH_PROVIDER_SERVICE_PORT");
|
||||
}
|
||||
if (oauth_provider_admin_service_url == null) {
|
||||
oauth_provider_admin_service_url = "http://localhost:" + System.getProperty("ST_OAUTH_PROVIDER_ADMIN_PORT");
|
||||
}
|
||||
if (oauth_provider_url_configured_in_oauth_provider == null) {
|
||||
oauth_provider_url_configured_in_oauth_provider = "http://localhost:4444";
|
||||
}
|
||||
if (oauth_client_secret_encryption_key == null) {
|
||||
oauth_client_secret_encryption_key = "clientsecretencryptionkey";
|
||||
}
|
||||
if (oauth_provider_consent_login_base_url == null) {
|
||||
oauth_provider_consent_login_base_url = "http://localhost:3001/auth";
|
||||
}
|
||||
}
|
||||
|
||||
isNormalizedAndValid = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0 (the
|
||||
* "License") as published by the Apache Software Foundation.
|
||||
*
|
||||
* You may not use this file except in compliance with the License. You may
|
||||
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package io.supertokens.config.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
// Make annotation accessible at runtime so that config can be read from env
|
||||
@Target(ElementType.FIELD) // Annotation can only be applied to fields
|
||||
public @interface EnvName {
|
||||
String value(); // String value that provides a env var name for the field
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import io.supertokens.bulkimport.BulkImportUserUtils;
|
|||
import io.supertokens.config.Config;
|
||||
import io.supertokens.cronjobs.CronTask;
|
||||
import io.supertokens.cronjobs.CronTaskTest;
|
||||
import io.supertokens.output.Logging;
|
||||
import io.supertokens.pluginInterface.STORAGE_TYPE;
|
||||
import io.supertokens.pluginInterface.StorageUtils;
|
||||
import io.supertokens.pluginInterface.bulkimport.BulkImportStorage;
|
||||
|
|
@ -46,7 +47,7 @@ import java.util.stream.Stream;
|
|||
|
||||
public class ProcessBulkImportUsers extends CronTask {
|
||||
|
||||
public static final String RESOURCE_KEY = "io.supertokens.ee.cronjobs.ProcessBulkImportUsers";
|
||||
public static final String RESOURCE_KEY = "io.supertokens.cronjobs.ProcessBulkImportUsers";
|
||||
|
||||
private ExecutorService executorService;
|
||||
|
||||
|
|
@ -72,32 +73,49 @@ public class ProcessBulkImportUsers extends CronTask {
|
|||
.getStorage(app.getAsPublicTenantIdentifier(), main);
|
||||
|
||||
//split the loaded users list into smaller chunks
|
||||
int NUMBER_OF_BATCHES = Config.getConfig(app.getAsPublicTenantIdentifier(), main)
|
||||
int numberOfBatchChunks = Config.getConfig(app.getAsPublicTenantIdentifier(), main)
|
||||
.getBulkMigrationParallelism();
|
||||
executorService = Executors.newFixedThreadPool(NUMBER_OF_BATCHES);
|
||||
int bulkMigrationBatchSize = Config.getConfig(app.getAsPublicTenantIdentifier(), main)
|
||||
.getBulkMigrationBatchSize();
|
||||
|
||||
Logging.debug(main, app.getAsPublicTenantIdentifier(), "CronTask starts. Instance: " + this);
|
||||
Logging.debug(main, app.getAsPublicTenantIdentifier(), "CronTask starts. Processing bulk import users with " + bulkMigrationBatchSize
|
||||
+ " batch size, one batch split into " + numberOfBatchChunks + " chunks");
|
||||
|
||||
executorService = Executors.newFixedThreadPool(numberOfBatchChunks);
|
||||
String[] allUserRoles = StorageUtils.getUserRolesStorage(bulkImportSQLStorage).getRoles(app);
|
||||
BulkImportUserUtils bulkImportUserUtils = new BulkImportUserUtils(allUserRoles);
|
||||
|
||||
long newUsers = bulkImportSQLStorage.getBulkImportUsersCount(app, BulkImportStorage.BULK_IMPORT_USER_STATUS.NEW);
|
||||
long processingUsers = bulkImportSQLStorage.getBulkImportUsersCount(app, BulkImportStorage.BULK_IMPORT_USER_STATUS.PROCESSING);
|
||||
long failedUsers = 0;
|
||||
//taking a "snapshot" here and processing in this round as many users as there are uploaded now. After this the processing will go on
|
||||
//with another app and gets back here when all the apps had a chance.
|
||||
long usersProcessed = 0;
|
||||
|
||||
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Found " + (newUsers + processingUsers) + " waiting for processing"
|
||||
+ " (" + newUsers + " new, " + processingUsers + " processing)");;
|
||||
|
||||
while(usersProcessed < (newUsers + processingUsers)) {
|
||||
|
||||
List<BulkImportUser> users = bulkImportSQLStorage.getBulkImportUsersAndChangeStatusToProcessing(app,
|
||||
BulkImport.PROCESS_USERS_BATCH_SIZE);
|
||||
bulkMigrationBatchSize);
|
||||
|
||||
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Loaded " + users.size() + " users to process");
|
||||
|
||||
if (users == null || users.isEmpty()) {
|
||||
// "No more users to process!"
|
||||
break;
|
||||
}
|
||||
|
||||
List<List<BulkImportUser>> loadedUsersChunks = makeChunksOf(users, NUMBER_OF_BATCHES);
|
||||
List<List<BulkImportUser>> loadedUsersChunks = makeChunksOf(users, numberOfBatchChunks);
|
||||
for (List<BulkImportUser> chunk : loadedUsersChunks) {
|
||||
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Chunk size: " + chunk.size());
|
||||
}
|
||||
|
||||
try {
|
||||
List<Future<?>> tasks = new ArrayList<>();
|
||||
for (int i = 0; i < NUMBER_OF_BATCHES && i < loadedUsersChunks.size(); i++) {
|
||||
for (int i = 0; i < numberOfBatchChunks && i < loadedUsersChunks.size(); i++) {
|
||||
tasks.add(
|
||||
executorService.submit(new ProcessBulkUsersImportWorker(main, app, loadedUsersChunks.get(i),
|
||||
bulkImportSQLStorage, bulkImportUserUtils)));
|
||||
|
|
@ -105,13 +123,30 @@ public class ProcessBulkImportUsers extends CronTask {
|
|||
|
||||
for (Future<?> task : tasks) {
|
||||
while (!task.isDone()) {
|
||||
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Waiting for task " + task + " to finish");
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
Void result = (Void) task.get(); //to know if there were any errors while executing and for waiting in this thread for all the other threads to finish up
|
||||
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Task " + task + " finished");
|
||||
try {
|
||||
Void result = (Void) task.get(); //to know if there were any errors while executing and for
|
||||
// waiting in this thread for all the other threads to finish up
|
||||
Logging.debug(main, app.getAsPublicTenantIdentifier(),
|
||||
"Task " + task + " finished with result: " + result);
|
||||
} catch (ExecutionException executionException) {
|
||||
Logging.error(main, app.getAsPublicTenantIdentifier(),
|
||||
"Error while processing bulk import users", true,
|
||||
executionException);
|
||||
throw new RuntimeException(executionException);
|
||||
}
|
||||
usersProcessed += loadedUsersChunks.get(tasks.indexOf(task)).size();
|
||||
failedUsers = bulkImportSQLStorage.getBulkImportUsersCount(app, BulkImportStorage.BULK_IMPORT_USER_STATUS.FAILED);
|
||||
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Chunk " + tasks.indexOf(task) + " finished processing, all chunks processed: "
|
||||
+ usersProcessed + " users (" + failedUsers + " failed)");
|
||||
}
|
||||
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Logging.debug(main, app.getAsPublicTenantIdentifier(), "Processing round finished");
|
||||
} catch (InterruptedException e) {
|
||||
Logging.error(main, app.getAsPublicTenantIdentifier(), "Error while processing bulk import users", true,
|
||||
e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ public class ProcessBulkUsersImportWorker implements Runnable {
|
|||
DbInitException {
|
||||
BulkImportUser user = null;
|
||||
try {
|
||||
Logging.debug(main, appIdentifier.getAsPublicTenantIdentifier(),
|
||||
"Processing bulk import users: " + users.size());
|
||||
final Storage[] allStoragesForApp = getAllProxyStoragesForApp(main, appIdentifier);
|
||||
int userIndexPointer = 0;
|
||||
List<BulkImportUser> validUsers = new ArrayList<>();
|
||||
|
|
@ -92,7 +94,7 @@ public class ProcessBulkUsersImportWorker implements Runnable {
|
|||
// Validate the user
|
||||
try {
|
||||
validUsers.add(bulkImportUserUtils.createBulkImportUserFromJSON(main, appIdentifier,
|
||||
user.toJsonObject(), user.id));
|
||||
user.toJsonObject(), BulkImportUserUtils.IDMode.READ_STORED));
|
||||
} catch (InvalidBulkImportDataException exception) {
|
||||
validationErrorsBeforeActualProcessing.put(user.id, new Exception(
|
||||
String.valueOf(exception.errors)));
|
||||
|
|
@ -107,12 +109,14 @@ public class ProcessBulkUsersImportWorker implements Runnable {
|
|||
// Since all the tenants of a user must share the storage, we will just use the
|
||||
// storage of the first tenantId of the first loginMethod
|
||||
Map<SQLStorage, List<BulkImportUser>> partitionedUsers = partitionUsersByStorage(appIdentifier, validUsers);
|
||||
|
||||
for(SQLStorage bulkImportProxyStorage : partitionedUsers.keySet()) {
|
||||
boolean shouldRetryImmediatley = true;
|
||||
while (shouldRetryImmediatley) {
|
||||
shouldRetryImmediatley = bulkImportProxyStorage.startTransaction(con -> {
|
||||
try {
|
||||
BulkImport.processUsersImportSteps(main, appIdentifier, bulkImportProxyStorage, partitionedUsers.get(bulkImportProxyStorage),
|
||||
BulkImport.processUsersImportSteps(main, appIdentifier, bulkImportProxyStorage,
|
||||
partitionedUsers.get(bulkImportProxyStorage),
|
||||
allStoragesForApp);
|
||||
|
||||
bulkImportProxyStorage.commitTransactionForBulkImportProxyStorage();
|
||||
|
|
@ -122,8 +126,18 @@ public class ProcessBulkUsersImportWorker implements Runnable {
|
|||
toDelete[i] = validUsers.get(i).id;
|
||||
}
|
||||
|
||||
baseTenantStorage.deleteBulkImportUsers(appIdentifier, toDelete);
|
||||
} catch (StorageTransactionLogicException e) {
|
||||
while (true){
|
||||
try {
|
||||
List<String> deletedIds = baseTenantStorage.deleteBulkImportUsers(appIdentifier,
|
||||
toDelete);
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
// ignore and retry delete. The import transaction is already committed, the delete should happen no matter what
|
||||
Logging.debug(main, app.getAsPublicTenantIdentifier(),
|
||||
"Exception while deleting bulk import users: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (StorageTransactionLogicException | StorageQueryException e) {
|
||||
// We need to rollback the transaction manually because we have overridden that in the proxy
|
||||
// storage
|
||||
bulkImportProxyStorage.rollbackTransactionForBulkImportProxyStorage();
|
||||
|
|
@ -138,9 +152,15 @@ public class ProcessBulkUsersImportWorker implements Runnable {
|
|||
}
|
||||
}
|
||||
} catch (StorageTransactionLogicException | InvalidConfigException e) {
|
||||
Logging.error(main, app.getAsPublicTenantIdentifier(),
|
||||
"Error while processing bulk import users: " + e.getMessage(), true, e);
|
||||
throw new RuntimeException(e);
|
||||
} catch (BulkImportBatchInsertException insertException) {
|
||||
handleProcessUserExceptions(app, users, insertException, baseTenantStorage);
|
||||
} catch (Exception e) {
|
||||
Logging.error(main, app.getAsPublicTenantIdentifier(),
|
||||
"Error while processing bulk import users: " + e.getMessage(), true, e);
|
||||
throw e;
|
||||
} finally {
|
||||
closeAllProxyStorages(); //closing it here to reuse the existing connection with all the users
|
||||
}
|
||||
|
|
@ -163,30 +183,40 @@ public class ProcessBulkUsersImportWorker implements Runnable {
|
|||
String[] errorMessage = { e.getMessage() };
|
||||
Map<String, String> bulkImportUserIdToErrorMessage = new HashMap<>();
|
||||
|
||||
if (e instanceof StorageTransactionLogicException) {
|
||||
StorageTransactionLogicException exception = (StorageTransactionLogicException) e;
|
||||
// If the exception is due to a StorageQueryException, we want to retry the entry after sometime instead
|
||||
// of marking it as FAILED. We will return early in that case.
|
||||
if (exception.actualException instanceof StorageQueryException) {
|
||||
Logging.error(main, null, "We got an StorageQueryException while processing a bulk import user entry. It will be retried again. Error Message: " + e.getMessage(), true);
|
||||
return;
|
||||
}
|
||||
if(exception.actualException instanceof BulkImportBatchInsertException){
|
||||
handleBulkImportException(usersBatch, (BulkImportBatchInsertException) exception.actualException, bulkImportUserIdToErrorMessage);
|
||||
} else {
|
||||
//fail the whole batch
|
||||
errorMessage[0] = exception.actualException.getMessage();
|
||||
for(BulkImportUser user : usersBatch){
|
||||
bulkImportUserIdToErrorMessage.put(user.id, errorMessage[0]);
|
||||
switch (e) {
|
||||
case StorageTransactionLogicException exception -> {
|
||||
// If the exception is due to a StorageQueryException, we want to retry the entry after sometime instead
|
||||
// of marking it as FAILED. We will return early in that case.
|
||||
if (exception.actualException instanceof StorageQueryException) {
|
||||
Logging.error(main, null,
|
||||
"We got an StorageQueryException while processing a bulk import user entry. It will be " +
|
||||
"retried again. Error Message: " +
|
||||
e.getMessage(), true);
|
||||
return;
|
||||
}
|
||||
if (exception.actualException instanceof BulkImportBatchInsertException) {
|
||||
handleBulkImportException(usersBatch, (BulkImportBatchInsertException) exception.actualException,
|
||||
bulkImportUserIdToErrorMessage);
|
||||
} else {
|
||||
//fail the whole batch
|
||||
errorMessage[0] = exception.actualException.getMessage();
|
||||
for (BulkImportUser user : usersBatch) {
|
||||
bulkImportUserIdToErrorMessage.put(user.id, errorMessage[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (e instanceof InvalidBulkImportDataException) {
|
||||
errorMessage[0] = ((InvalidBulkImportDataException) e).errors.toString();
|
||||
} else if (e instanceof InvalidConfigException) {
|
||||
errorMessage[0] = e.getMessage();
|
||||
} else if (e instanceof BulkImportBatchInsertException) {
|
||||
handleBulkImportException(usersBatch, (BulkImportBatchInsertException) e, bulkImportUserIdToErrorMessage);
|
||||
case InvalidBulkImportDataException invalidBulkImportDataException ->
|
||||
errorMessage[0] = invalidBulkImportDataException.errors.toString();
|
||||
case InvalidConfigException invalidConfigException -> errorMessage[0] = e.getMessage();
|
||||
case BulkImportBatchInsertException bulkImportBatchInsertException ->
|
||||
handleBulkImportException(usersBatch, bulkImportBatchInsertException,
|
||||
bulkImportUserIdToErrorMessage);
|
||||
default -> {
|
||||
Logging.error(main, null,
|
||||
"We got an error while processing a bulk import user entry. It will be " +
|
||||
"retried again. Error Message: " +
|
||||
e.getMessage(), true);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -205,7 +235,7 @@ public class ProcessBulkUsersImportWorker implements Runnable {
|
|||
Map<String, Exception> userIndexToError = exception.exceptionByUserId;
|
||||
for(String userid : userIndexToError.keySet()){
|
||||
Optional<BulkImportUser> userWithId = usersBatch.stream()
|
||||
.filter(bulkImportUser -> bulkImportUser.id.equals(userid) || bulkImportUser.externalUserId.equals(userid)).findFirst();
|
||||
.filter(bulkImportUser -> userid.equals(bulkImportUser.id) || userid.equals(bulkImportUser.externalUserId)).findFirst();
|
||||
String id = null;
|
||||
if(userWithId.isPresent()){
|
||||
id = userWithId.get().id;
|
||||
|
|
@ -284,7 +314,7 @@ public class ProcessBulkUsersImportWorker implements Runnable {
|
|||
Map<SQLStorage, List<BulkImportUser>> result = new HashMap<>();
|
||||
for(BulkImportUser user: users) {
|
||||
TenantIdentifier firstTenantIdentifier = new TenantIdentifier(appIdentifier.getConnectionUriDomain(),
|
||||
appIdentifier.getAppId(), user.loginMethods.get(0).tenantIds.get(0));
|
||||
appIdentifier.getAppId(), user.loginMethods.getFirst().tenantIds.getFirst());
|
||||
|
||||
SQLStorage bulkImportProxyStorage = (SQLStorage) getBulkImportProxyStorage(firstTenantIdentifier);
|
||||
if(!result.containsKey(bulkImportProxyStorage)){
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0 (the
|
||||
* "License") as published by the Apache Software Foundation.
|
||||
*
|
||||
* You may not use this file except in compliance with the License. You may
|
||||
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package io.supertokens.cronjobs.cleanupWebauthnExpiredData;
|
||||
|
||||
import io.supertokens.Main;
|
||||
import io.supertokens.cronjobs.CronTask;
|
||||
import io.supertokens.cronjobs.CronTaskTest;
|
||||
import io.supertokens.pluginInterface.STORAGE_TYPE;
|
||||
import io.supertokens.pluginInterface.Storage;
|
||||
import io.supertokens.pluginInterface.StorageUtils;
|
||||
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
||||
import io.supertokens.pluginInterface.webauthn.WebAuthNStorage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CleanUpWebauthNExpiredDataCron extends CronTask {
|
||||
|
||||
public static final String RESOURCE_KEY = "io.supertokens.cronjobs.cleanupWebauthnExpiredData" +
|
||||
".CleanUpWebauthnExpiredDataCron";
|
||||
|
||||
private CleanUpWebauthNExpiredDataCron(Main main, List<List<TenantIdentifier>> tenantsInfo) {
|
||||
super("CleanUpWebauthnExpiredDataCron", main, tenantsInfo, true);
|
||||
}
|
||||
|
||||
public static CleanUpWebauthNExpiredDataCron init(Main main, List<List<TenantIdentifier>> tenantsInfo) {
|
||||
return (CleanUpWebauthNExpiredDataCron) main.getResourceDistributor()
|
||||
.setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY,
|
||||
new CleanUpWebauthNExpiredDataCron(main, tenantsInfo));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doTaskPerStorage(Storage storage) throws Exception {
|
||||
if (storage.getType() != STORAGE_TYPE.SQL) {
|
||||
return;
|
||||
}
|
||||
|
||||
WebAuthNStorage webAuthNStorage = StorageUtils.getWebAuthNStorage(storage);
|
||||
webAuthNStorage.deleteExpiredAccountRecoveryTokens();
|
||||
webAuthNStorage.deleteExpiredGeneratedOptions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntervalTimeSeconds() {
|
||||
if (Main.isTesting) {
|
||||
Integer interval = CronTaskTest.getInstance(main).getIntervalInSeconds(RESOURCE_KEY);
|
||||
if (interval != null) {
|
||||
return interval;
|
||||
}
|
||||
}
|
||||
// Every 24 hours.
|
||||
return 24 * 3600;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInitialWaitTimeSeconds() {
|
||||
if (!Main.isTesting) {
|
||||
return getIntervalTimeSeconds();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0 (the
|
||||
* "License") as published by the Apache Software Foundation.
|
||||
*
|
||||
* You may not use this file except in compliance with the License. You may
|
||||
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package io.supertokens.cronjobs.deadlocklogger;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.ThreadInfo;
|
||||
import java.lang.management.ThreadMXBean;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class DeadlockLogger {
|
||||
|
||||
private static final DeadlockLogger INSTANCE = new DeadlockLogger();
|
||||
|
||||
private DeadlockLogger() {
|
||||
}
|
||||
|
||||
public static DeadlockLogger getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void start(){
|
||||
Thread deadlockLoggerThread = new Thread(deadlockDetector, "DeadlockLoggerThread");
|
||||
deadlockLoggerThread.setDaemon(true);
|
||||
deadlockLoggerThread.start();
|
||||
}
|
||||
|
||||
private final Runnable deadlockDetector = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("DeadlockLogger started!");
|
||||
while (true) {
|
||||
System.out.println("DeadlockLogger - checking");
|
||||
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
|
||||
long[] threadIds = bean.findDeadlockedThreads(); // Returns null if no threads are deadlocked.
|
||||
System.out.println("DeadlockLogger - DeadlockedThreads: " + Arrays.toString(threadIds));
|
||||
if (threadIds != null) {
|
||||
ThreadInfo[] infos = bean.getThreadInfo(threadIds);
|
||||
boolean deadlockFound = false;
|
||||
System.out.println("DEADLOCK found!");
|
||||
for (ThreadInfo info : infos) {
|
||||
System.out.println("ThreadName: " + info.getThreadName());
|
||||
System.out.println("Thread ID: " + info.getThreadId());
|
||||
System.out.println("LockName: " + info.getLockName());
|
||||
System.out.println("LockOwnerName: " + info.getLockOwnerName());
|
||||
System.out.println("LockedMonitors: " + Arrays.toString(info.getLockedMonitors()));
|
||||
System.out.println("LockInfo: " + info.getLockInfo());
|
||||
System.out.println("Stack: " + Arrays.toString(info.getStackTrace()));
|
||||
System.out.println();
|
||||
deadlockFound = true;
|
||||
}
|
||||
System.out.println("*******************************");
|
||||
if(deadlockFound) {
|
||||
System.out.println(" ==== ALL THREAD INFO ===");
|
||||
ThreadInfo[] allThreads = bean.dumpAllThreads(true, true, 100);
|
||||
for (ThreadInfo threadInfo : allThreads) {
|
||||
System.out.println("THREAD: " + threadInfo.getThreadName());
|
||||
System.out.println("StackTrace: " + Arrays.toString(threadInfo.getStackTrace()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(10000);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package io.supertokens.cronjobs.deleteExpiredSAMLData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.supertokens.Main;
|
||||
import io.supertokens.cronjobs.CronTask;
|
||||
import io.supertokens.cronjobs.CronTaskTest;
|
||||
import io.supertokens.pluginInterface.Storage;
|
||||
import io.supertokens.pluginInterface.StorageUtils;
|
||||
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
||||
import io.supertokens.pluginInterface.saml.SAMLStorage;
|
||||
|
||||
public class DeleteExpiredSAMLData extends CronTask {
|
||||
public static final String RESOURCE_KEY = "io.supertokens.cronjobs.deleteExpiredSAMLData" +
|
||||
".DeleteExpiredSAMLData";
|
||||
|
||||
private DeleteExpiredSAMLData(Main main, List<List<TenantIdentifier>> tenantsInfo) {
|
||||
super("DeleteExpiredSAMLData", main, tenantsInfo, false);
|
||||
}
|
||||
|
||||
public static DeleteExpiredSAMLData init(Main main, List<List<TenantIdentifier>> tenantsInfo) {
|
||||
return (DeleteExpiredSAMLData) main.getResourceDistributor()
|
||||
.setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY,
|
||||
new DeleteExpiredSAMLData(main, tenantsInfo));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doTaskPerStorage(Storage storage) throws Exception {
|
||||
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
|
||||
samlStorage.removeExpiredSAMLCodesAndRelayStates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntervalTimeSeconds() {
|
||||
if (Main.isTesting) {
|
||||
Integer interval = CronTaskTest.getInstance(main).getIntervalInSeconds(RESOURCE_KEY);
|
||||
if (interval != null) {
|
||||
return interval;
|
||||
}
|
||||
}
|
||||
// Every hour
|
||||
return 3600;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInitialWaitTimeSeconds() {
|
||||
if (!Main.isTesting) {
|
||||
return getIntervalTimeSeconds();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import io.supertokens.cronjobs.CronTaskTest;
|
|||
import io.supertokens.output.Logging;
|
||||
import io.supertokens.pluginInterface.STORAGE_TYPE;
|
||||
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
||||
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
|
||||
import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage;
|
||||
import io.supertokens.storageLayer.StorageLayer;
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
|
|
@ -30,7 +31,11 @@ public class DeleteExpiredTotpTokens extends CronTask {
|
|||
|
||||
@TestOnly
|
||||
public static DeleteExpiredTotpTokens getInstance(Main main) {
|
||||
return (DeleteExpiredTotpTokens) main.getResourceDistributor().getResource(RESOURCE_KEY);
|
||||
try {
|
||||
return (DeleteExpiredTotpTokens) main.getResourceDistributor().getResource(TenantIdentifier.BASE_TENANT, RESOURCE_KEY);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -19,12 +19,9 @@ package io.supertokens.cronjobs.syncCoreConfigWithDb;
|
|||
import io.supertokens.Main;
|
||||
import io.supertokens.cronjobs.CronTask;
|
||||
import io.supertokens.cronjobs.CronTaskTest;
|
||||
import io.supertokens.cronjobs.deleteExpiredSessions.DeleteExpiredSessions;
|
||||
import io.supertokens.multitenancy.Multitenancy;
|
||||
import io.supertokens.multitenancy.MultitenancyHelper;
|
||||
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
||||
|
||||
import java.util.List;
|
||||
import io.supertokens.pluginInterface.opentelemetry.WithinOtelSpan;
|
||||
|
||||
public class SyncCoreConfigWithDb extends CronTask {
|
||||
|
||||
|
|
@ -62,6 +59,7 @@ public class SyncCoreConfigWithDb extends CronTask {
|
|||
return 60;
|
||||
}
|
||||
|
||||
@WithinOtelSpan
|
||||
@Override
|
||||
protected void doTaskForTargetTenant(TenantIdentifier targetTenant) throws Exception {
|
||||
MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true);
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ public class Telemetry extends CronTask {
|
|||
json.add("maus", new JsonArray());
|
||||
}
|
||||
|
||||
String url = "https://api.supertokens.io/0/st/telemetry";
|
||||
String url = "https://api.supertokens.com/0/st/telemetry";
|
||||
|
||||
// we call the API only if we are not testing the core, of if the request can be mocked (in case a test
|
||||
// wants
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.supertokens.dashboard;
|
||||
|
||||
import io.supertokens.Main;
|
||||
import io.supertokens.ResourceDistributor;
|
||||
import io.supertokens.dashboard.exceptions.UserSuspendedException;
|
||||
import io.supertokens.emailpassword.PasswordHashing;
|
||||
import io.supertokens.featureflag.EE_FEATURES;
|
||||
|
|
@ -55,7 +56,7 @@ public class Dashboard {
|
|||
throws StorageQueryException, DuplicateEmailException, FeatureNotEnabledException {
|
||||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return signUpDashboardUser(new AppIdentifier(null, null), storage,
|
||||
return signUpDashboardUser(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
|
||||
main, email, password);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -103,7 +104,7 @@ public class Dashboard {
|
|||
public static DashboardUser[] getAllDashboardUsers(Main main)
|
||||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getAllDashboardUsers(new AppIdentifier(null, null), storage, main);
|
||||
return getAllDashboardUsers(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, main);
|
||||
}
|
||||
|
||||
public static DashboardUser[] getAllDashboardUsers(AppIdentifier appIdentifier, Storage storage, Main main)
|
||||
|
|
@ -127,7 +128,7 @@ public class Dashboard {
|
|||
throws StorageQueryException, UserSuspendedException {
|
||||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return signInDashboardUser(new AppIdentifier(null, null), storage,
|
||||
return signInDashboardUser(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
|
||||
main, email, password);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -159,7 +160,7 @@ public class Dashboard {
|
|||
public static boolean deleteUserWithUserId(Main main, String userId)
|
||||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return deleteUserWithUserId(new AppIdentifier(null, null), storage, userId);
|
||||
return deleteUserWithUserId(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId);
|
||||
}
|
||||
|
||||
public static boolean deleteUserWithUserId(AppIdentifier appIdentifier, Storage storage, String userId)
|
||||
|
|
@ -201,7 +202,7 @@ public class Dashboard {
|
|||
public static boolean deleteUserWithEmail(Main main, String email)
|
||||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return deleteUserWithEmail(new AppIdentifier(null, null), storage, email);
|
||||
return deleteUserWithEmail(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, email);
|
||||
}
|
||||
|
||||
public static boolean deleteUserWithEmail(AppIdentifier appIdentifier, Storage storage, String email)
|
||||
|
|
@ -223,7 +224,7 @@ public class Dashboard {
|
|||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return updateUsersCredentialsWithUserId(
|
||||
new AppIdentifier(null, null), storage, main, userId,
|
||||
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, main, userId,
|
||||
newEmail, newPassword);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -291,7 +292,7 @@ public class Dashboard {
|
|||
public static DashboardUser getDashboardUserByEmail(Main main, String email)
|
||||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getDashboardUserByEmail(new AppIdentifier(null, null), storage, email);
|
||||
return getDashboardUserByEmail(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, email);
|
||||
}
|
||||
|
||||
public static DashboardUser getDashboardUserByEmail(AppIdentifier appIdentifier, Storage storage, String email)
|
||||
|
|
@ -305,7 +306,7 @@ public class Dashboard {
|
|||
public static boolean revokeSessionWithSessionId(Main main, String sessionId)
|
||||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return revokeSessionWithSessionId(new AppIdentifier(null, null), storage, sessionId);
|
||||
return revokeSessionWithSessionId(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, sessionId);
|
||||
}
|
||||
|
||||
public static boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, Storage storage, String sessionId)
|
||||
|
|
@ -320,7 +321,7 @@ public class Dashboard {
|
|||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getAllDashboardSessionsForUser(
|
||||
new AppIdentifier(null, null), storage, userId);
|
||||
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId);
|
||||
}
|
||||
|
||||
public static DashboardSessionInfo[] getAllDashboardSessionsForUser(AppIdentifier appIdentifier, Storage storage,
|
||||
|
|
@ -390,7 +391,7 @@ public class Dashboard {
|
|||
public static boolean isValidUserSession(Main main, String sessionId)
|
||||
throws StorageQueryException, UserSuspendedException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return isValidUserSession(new AppIdentifier(null, null), storage, main, sessionId);
|
||||
return isValidUserSession(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, main, sessionId);
|
||||
}
|
||||
|
||||
public static boolean isValidUserSession(AppIdentifier appIdentifier, Storage storage, Main main, String sessionId)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,18 @@
|
|||
|
||||
package io.supertokens.emailpassword;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
|
||||
import io.supertokens.Main;
|
||||
import io.supertokens.ResourceDistributor;
|
||||
import io.supertokens.authRecipe.AuthRecipe;
|
||||
import io.supertokens.config.Config;
|
||||
import io.supertokens.config.CoreConfig;
|
||||
|
|
@ -50,14 +61,6 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoun
|
|||
import io.supertokens.storageLayer.StorageLayer;
|
||||
import io.supertokens.utils.Utils;
|
||||
import io.supertokens.webserver.WebserverAPI;
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.List;
|
||||
|
||||
public class EmailPassword {
|
||||
|
||||
|
|
@ -74,7 +77,7 @@ public class EmailPassword {
|
|||
@TestOnly
|
||||
public static long getPasswordResetTokenLifetimeForTests(Main main) {
|
||||
try {
|
||||
return getPasswordResetTokenLifetime(new TenantIdentifier(null, null, null), main);
|
||||
return getPasswordResetTokenLifetime(ResourceDistributor.getAppForTesting(), main);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
|
@ -90,7 +93,7 @@ public class EmailPassword {
|
|||
throws DuplicateEmailException, StorageQueryException {
|
||||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return signUp(new TenantIdentifier(null, null, null), storage,
|
||||
return signUp(ResourceDistributor.getAppForTesting(), storage,
|
||||
main, email, password);
|
||||
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -157,7 +160,7 @@ public class EmailPassword {
|
|||
Storage storage = StorageLayer.getStorage(main);
|
||||
|
||||
return importUserWithPasswordHash(
|
||||
new TenantIdentifier(null, null, null), storage, main, email,
|
||||
ResourceDistributor.getAppForTesting(), storage, main, email,
|
||||
passwordHash, hashingAlgorithm);
|
||||
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -215,7 +218,7 @@ public class EmailPassword {
|
|||
|
||||
public static ImportUserResponse createUserWithPasswordHash(TenantIdentifier tenantIdentifier, Storage storage,
|
||||
@Nonnull String email,
|
||||
@Nonnull String passwordHash, @Nullable long timeJoined)
|
||||
@Nonnull String passwordHash, long timeJoined)
|
||||
throws StorageQueryException, DuplicateEmailException, TenantOrAppNotFoundException,
|
||||
StorageTransactionLogicException {
|
||||
EmailPasswordSQLStorage epStorage = StorageUtils.getEmailPasswordStorage(storage);
|
||||
|
|
@ -276,7 +279,7 @@ public class EmailPassword {
|
|||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return importUserWithPasswordHash(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
main, email, passwordHash, null);
|
||||
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -289,7 +292,7 @@ public class EmailPassword {
|
|||
throws StorageQueryException, WrongCredentialsException {
|
||||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return signIn(new TenantIdentifier(null, null, null), storage,
|
||||
return signIn(ResourceDistributor.getAppForTesting(), storage,
|
||||
main, email, password);
|
||||
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -353,7 +356,7 @@ public class EmailPassword {
|
|||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return generatePasswordResetTokenBeforeCdi4_0(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
main, userId);
|
||||
} catch (TenantOrAppNotFoundException | BadPermissionException | WebserverAPI.BadRequestException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -366,7 +369,7 @@ public class EmailPassword {
|
|||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return generatePasswordResetToken(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
main, userId, null);
|
||||
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -379,7 +382,7 @@ public class EmailPassword {
|
|||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return generatePasswordResetToken(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
main, userId, email);
|
||||
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -456,7 +459,7 @@ public class EmailPassword {
|
|||
StorageTransactionLogicException {
|
||||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return resetPassword(new TenantIdentifier(null, null, null), storage,
|
||||
return resetPassword(ResourceDistributor.getAppForTesting(), storage,
|
||||
main, token, password);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -530,7 +533,7 @@ public class EmailPassword {
|
|||
StorageTransactionLogicException {
|
||||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return consumeResetPasswordToken(new TenantIdentifier(null, null, null), storage,
|
||||
return consumeResetPasswordToken(ResourceDistributor.getAppForTesting(), storage,
|
||||
token);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -628,7 +631,7 @@ public class EmailPassword {
|
|||
UnknownUserIdException, DuplicateEmailException, EmailChangeNotAllowedException {
|
||||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
updateUsersEmailOrPassword(new AppIdentifier(null, null), storage,
|
||||
updateUsersEmailOrPassword(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
|
||||
main, userId, email, password);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -725,7 +728,7 @@ public class EmailPassword {
|
|||
throws StorageQueryException {
|
||||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getUserUsingId(new AppIdentifier(null, null), storage, userId);
|
||||
return getUserUsingId(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoun
|
|||
import org.jetbrains.annotations.TestOnly;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
|
|
@ -42,6 +44,9 @@ public class PasswordHashing extends ResourceDistributor.SingletonResource {
|
|||
final BlockingQueue<Object> firebaseSCryptBoundedQueue;
|
||||
final Main main;
|
||||
|
||||
private final Map<String, String> cachedPasswordHashForTesting = new HashMap<>();
|
||||
public static boolean bypassHashCachingInTesting = false;
|
||||
|
||||
private PasswordHashing(Main main) {
|
||||
this.argon2BoundedQueue = new LinkedBlockingQueue<>(
|
||||
Config.getBaseConfig(main).getArgon2HashingPoolSize());
|
||||
|
|
@ -75,7 +80,7 @@ public class PasswordHashing extends ResourceDistributor.SingletonResource {
|
|||
@TestOnly
|
||||
public String createHashWithSalt(String password) {
|
||||
try {
|
||||
return createHashWithSalt(new AppIdentifier(null, null), password);
|
||||
return createHashWithSalt(ResourceDistributor.getAppForTesting().toAppIdentifier(), password);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
|
@ -84,6 +89,10 @@ public class PasswordHashing extends ResourceDistributor.SingletonResource {
|
|||
public String createHashWithSalt(AppIdentifier appIdentifier, String password)
|
||||
throws TenantOrAppNotFoundException {
|
||||
|
||||
if (Main.isTesting && !bypassHashCachingInTesting && cachedPasswordHashForTesting.containsKey(password)) {
|
||||
return cachedPasswordHashForTesting.get(password);
|
||||
}
|
||||
|
||||
String passwordHash = "";
|
||||
|
||||
TenantIdentifier tenantIdentifier = appIdentifier.getAsPublicTenantIdentifier();
|
||||
|
|
@ -108,6 +117,10 @@ public class PasswordHashing extends ResourceDistributor.SingletonResource {
|
|||
} catch (UnsupportedPasswordHashingFormatException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
||||
if (Main.isTesting) {
|
||||
cachedPasswordHashForTesting.put(password, passwordHash);
|
||||
}
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import io.supertokens.config.CoreConfig;
|
|||
import io.supertokens.emailpassword.exceptions.UnsupportedPasswordHashingFormatException;
|
||||
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
|
||||
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
|
||||
import org.apache.tomcat.util.codec.binary.Base64;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.crypto.Cipher;
|
||||
|
|
@ -118,9 +118,9 @@ public class PasswordHashingUtils {
|
|||
|
||||
// concatenating decoded salt + separator
|
||||
byte[] byteArrTemp = response.salt.getBytes(StandardCharsets.US_ASCII);
|
||||
byte[] decodedSaltBytes = Base64.decodeBase64(byteArrTemp, 0, byteArrTemp.length);
|
||||
byte[] decodedSaltBytes = Base64.decodeBase64(byteArrTemp);
|
||||
byteArrTemp = response.saltSeparator.getBytes(StandardCharsets.US_ASCII);
|
||||
byte[] decodedSaltSepBytes = Base64.decodeBase64(byteArrTemp, 0, byteArrTemp.length);
|
||||
byte[] decodedSaltSepBytes = Base64.decodeBase64(byteArrTemp);
|
||||
|
||||
byte[] saltConcat = new byte[decodedSaltBytes.length + decodedSaltSepBytes.length];
|
||||
System.arraycopy(decodedSaltBytes, 0, saltConcat, 0, decodedSaltBytes.length);
|
||||
|
|
@ -136,7 +136,7 @@ public class PasswordHashingUtils {
|
|||
}
|
||||
// encrypting with aes
|
||||
byteArrTemp = base64_signer_key.getBytes(StandardCharsets.US_ASCII);
|
||||
byte[] signerBytes = Base64.decodeBase64(byteArrTemp, 0, byteArrTemp.length);
|
||||
byte[] signerBytes = Base64.decodeBase64(byteArrTemp);
|
||||
|
||||
try {
|
||||
String CIPHER = "AES/CTR/NoPadding";
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.supertokens.emailverification;
|
||||
|
||||
import io.supertokens.Main;
|
||||
import io.supertokens.ResourceDistributor;
|
||||
import io.supertokens.config.Config;
|
||||
import io.supertokens.emailverification.exception.EmailAlreadyVerifiedException;
|
||||
import io.supertokens.emailverification.exception.EmailVerificationInvalidTokenException;
|
||||
|
|
@ -44,7 +45,7 @@ public class EmailVerification {
|
|||
public static long getEmailVerificationTokenLifetimeForTests(Main main) {
|
||||
try {
|
||||
return getEmailVerificationTokenLifetime(
|
||||
new TenantIdentifier(null, null, null), main);
|
||||
ResourceDistributor.getAppForTesting(), main);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
|
@ -62,7 +63,7 @@ public class EmailVerification {
|
|||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return generateEmailVerificationToken(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
main, userId, email);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -107,7 +108,7 @@ public class EmailVerification {
|
|||
EmailVerificationInvalidTokenException, NoSuchAlgorithmException, StorageTransactionLogicException {
|
||||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return verifyEmail(new TenantIdentifier(null, null, null), storage, token);
|
||||
return verifyEmail(ResourceDistributor.getAppForTesting(), storage, token);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
|
@ -182,7 +183,7 @@ public class EmailVerification {
|
|||
public static boolean isEmailVerified(Main main, String userId,
|
||||
String email) throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return isEmailVerified(new AppIdentifier(null, null), storage,
|
||||
return isEmailVerified(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
|
||||
userId, email);
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +197,7 @@ public class EmailVerification {
|
|||
public static void revokeAllTokens(Main main, String userId,
|
||||
String email) throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
revokeAllTokens(new TenantIdentifier(null, null, null), storage,
|
||||
revokeAllTokens(ResourceDistributor.getAppForTesting(), storage,
|
||||
userId, email);
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +212,7 @@ public class EmailVerification {
|
|||
String email) throws StorageQueryException {
|
||||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
unverifyEmail(new AppIdentifier(null, null), storage, userId, email);
|
||||
unverifyEmail(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId, email);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
|
@ -249,7 +250,7 @@ public class EmailVerification {
|
|||
|
||||
try {
|
||||
StorageUtils.getEmailVerificationStorage(StorageLayer.getStorage(main))
|
||||
.addEmailVerificationToken(new TenantIdentifier(null, null, null),
|
||||
.addEmailVerificationToken(ResourceDistributor.getAppForTesting(),
|
||||
new EmailVerificationTokenInfo(userId, hashedToken,
|
||||
System.currentTimeMillis() +
|
||||
EmailVerification.getEmailVerificationTokenLifetimeForTests(main), email));
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package io.supertokens.featureflag;
|
|||
|
||||
public enum EE_FEATURES {
|
||||
ACCOUNT_LINKING("account_linking"), MULTI_TENANCY("multi_tenancy"), TEST("test"),
|
||||
DASHBOARD_LOGIN("dashboard_login"), MFA("mfa"), SECURITY("security"), OAUTH("oauth");
|
||||
DASHBOARD_LOGIN("dashboard_login"), MFA("mfa"), SECURITY("security"), OAUTH("oauth"), SAML("saml");
|
||||
|
||||
private final String name;
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ public class FeatureFlag extends ResourceDistributor.SingletonResource {
|
|||
public static FeatureFlag getInstance(Main main) {
|
||||
try {
|
||||
return (FeatureFlag) main.getResourceDistributor()
|
||||
.getResource(new AppIdentifier(null, null), RESOURCE_KEY);
|
||||
.getResource(ResourceDistributor.getAppForTesting(), RESOURCE_KEY);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,11 +65,16 @@ import io.supertokens.pluginInterface.oauth.OAuthLogoutChallenge;
|
|||
import io.supertokens.pluginInterface.oauth.OAuthStorage;
|
||||
import io.supertokens.pluginInterface.oauth.exception.DuplicateOAuthLogoutChallengeException;
|
||||
import io.supertokens.pluginInterface.oauth.exception.OAuthClientNotFoundException;
|
||||
import io.supertokens.pluginInterface.opentelemetry.OtelProvider;
|
||||
import io.supertokens.pluginInterface.passwordless.PasswordlessCode;
|
||||
import io.supertokens.pluginInterface.passwordless.PasswordlessDevice;
|
||||
import io.supertokens.pluginInterface.passwordless.PasswordlessImportUser;
|
||||
import io.supertokens.pluginInterface.passwordless.exception.*;
|
||||
import io.supertokens.pluginInterface.passwordless.sqlStorage.PasswordlessSQLStorage;
|
||||
import io.supertokens.pluginInterface.saml.SAMLClaimsInfo;
|
||||
import io.supertokens.pluginInterface.saml.SAMLClient;
|
||||
import io.supertokens.pluginInterface.saml.SAMLRelayStateInfo;
|
||||
import io.supertokens.pluginInterface.saml.SAMLStorage;
|
||||
import io.supertokens.pluginInterface.session.SessionInfo;
|
||||
import io.supertokens.pluginInterface.session.SessionStorage;
|
||||
import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage;
|
||||
|
|
@ -96,6 +101,11 @@ import io.supertokens.pluginInterface.userroles.UserRolesStorage;
|
|||
import io.supertokens.pluginInterface.userroles.exception.DuplicateUserRoleMappingException;
|
||||
import io.supertokens.pluginInterface.userroles.exception.UnknownRoleException;
|
||||
import io.supertokens.pluginInterface.userroles.sqlStorage.UserRolesSQLStorage;
|
||||
import io.supertokens.pluginInterface.webauthn.AccountRecoveryTokenInfo;
|
||||
import io.supertokens.pluginInterface.webauthn.WebAuthNOptions;
|
||||
import io.supertokens.pluginInterface.webauthn.WebAuthNStoredCredential;
|
||||
import io.supertokens.pluginInterface.webauthn.exceptions.*;
|
||||
import io.supertokens.pluginInterface.webauthn.slqStorage.WebAuthNSQLStorage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
import org.sqlite.SQLiteException;
|
||||
|
|
@ -111,7 +121,8 @@ public class Start
|
|||
implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage,
|
||||
JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage,
|
||||
UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, TOTPSQLStorage, ActiveUsersStorage,
|
||||
ActiveUsersSQLStorage, DashboardSQLStorage, AuthRecipeSQLStorage, OAuthStorage {
|
||||
ActiveUsersSQLStorage, DashboardSQLStorage, AuthRecipeSQLStorage, OAuthStorage, WebAuthNSQLStorage,
|
||||
SAMLStorage {
|
||||
|
||||
private static final Object appenderLock = new Object();
|
||||
private static final String ACCESS_TOKEN_SIGNING_KEY_NAME = "access_token_signing_key";
|
||||
|
|
@ -197,7 +208,7 @@ public class Start
|
|||
}
|
||||
|
||||
@Override
|
||||
public void initFileLogging(String infoLogPath, String errorLogPath) {
|
||||
public void initFileLogging(String infoLogPath, String errorLogPath, OtelProvider otelProvider) {
|
||||
// no op
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +233,7 @@ public class Start
|
|||
@Override
|
||||
public <T> T startTransaction(TransactionLogic<T> logic)
|
||||
throws StorageTransactionLogicException, StorageQueryException {
|
||||
return startTransaction(logic, TransactionIsolationLevel.SERIALIZABLE);
|
||||
return startTransaction(logic, TransactionIsolationLevel.READ_COMMITTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -614,6 +625,11 @@ public class Start
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateConfigJsonFromEnv(JsonObject configJson) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, String className, String userId)
|
||||
throws StorageQueryException {
|
||||
|
|
@ -754,6 +770,8 @@ public class Start
|
|||
//ignore
|
||||
} else if (className.equals(OAuthStorage.class.getName())) {
|
||||
/* Since OAuth tables store client-related data, we don't add user-specific data here */
|
||||
} else if (className.equals(SAMLStorage.class.getName())) {
|
||||
// no user specific data here
|
||||
} else if (className.equals(ActiveUsersStorage.class.getName())) {
|
||||
try {
|
||||
ActiveUsersQueries.updateUserLastActive(this, tenantIdentifier.toAppIdentifier(), userId);
|
||||
|
|
@ -1011,7 +1029,8 @@ public class Start
|
|||
|
||||
@Override
|
||||
public void updateMultipleIsEmailVerified_Transaction(AppIdentifier appIdentifier, TransactionConnection con,
|
||||
Map<String, String> emailToUserId, boolean isEmailVerified)
|
||||
Map<String, String> emailToUserId,
|
||||
boolean isEmailVerified)
|
||||
throws StorageQueryException, TenantOrAppNotFoundException {
|
||||
Connection sqlCon = (Connection) con.getConnection();
|
||||
try {
|
||||
|
|
@ -1394,6 +1413,17 @@ public class Start
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthRecipeUserInfo getPrimaryUserByWebauthNCredentialId(TenantIdentifier tenantIdentifier,
|
||||
String webauthNCredentialId)
|
||||
throws StorageQueryException {
|
||||
try {
|
||||
return GeneralQueries.getPrimaryUserByWebauthNCredentialId(this, tenantIdentifier, webauthNCredentialId);
|
||||
} catch (SQLException | StorageTransactionLogicException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthRecipeUserInfo getPrimaryUserByThirdPartyInfo(TenantIdentifier tenantIdentifier, String thirdPartyId,
|
||||
String thirdPartyUserId) throws StorageQueryException {
|
||||
|
|
@ -2732,7 +2762,7 @@ public class Start
|
|||
try {
|
||||
startTransaction(con -> {
|
||||
try {
|
||||
createDevice_Transaction(con, new AppIdentifier(null, null), device);
|
||||
createDevice_Transaction(con, appIdentifier, device);
|
||||
} catch (DeviceAlreadyExistsException | TenantOrAppNotFoundException e) {
|
||||
throw new StorageTransactionLogicException(e);
|
||||
}
|
||||
|
|
@ -2987,6 +3017,19 @@ public class Start
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthRecipeUserInfo getPrimaryUserByWebauthNCredentialId_Transaction(TenantIdentifier tenantIdentifier,
|
||||
TransactionConnection con,
|
||||
String credentialId)
|
||||
throws StorageQueryException {
|
||||
try {
|
||||
Connection sqlCon = (Connection) con.getConnection();
|
||||
return GeneralQueries.getPrimaryUserByWebauthNCredentialId_Transaction(this, sqlCon, tenantIdentifier, credentialId);
|
||||
} catch (SQLException | StorageTransactionLogicException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuthRecipeUserInfo> getPrimaryUsersByIds_Transaction(AppIdentifier appIdentifier,
|
||||
TransactionConnection con, List<String> userIds)
|
||||
|
|
@ -3488,4 +3531,444 @@ public class Start
|
|||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthNStoredCredential saveCredentials(TenantIdentifier tenantIdentifier, WebAuthNStoredCredential credential)
|
||||
throws StorageQueryException, DuplicateCredentialException,
|
||||
io.supertokens.pluginInterface.webauthn.exceptions.UserIdNotFoundException, TenantOrAppNotFoundException {
|
||||
try {
|
||||
return WebAuthNQueries.saveCredential(this, tenantIdentifier, credential);
|
||||
} catch (SQLException e) {
|
||||
if (e instanceof SQLiteException) {
|
||||
SQLiteConfig config = Config.getConfig(this);
|
||||
String serverMessage = e.getMessage();
|
||||
|
||||
if (isPrimaryKeyError(serverMessage, config.getWebAuthNCredentialsTable(),
|
||||
new String[]{"app_id", "user_id", "id"})) {
|
||||
throw new io.supertokens.pluginInterface.webauthn.exceptions.DuplicateCredentialException();
|
||||
} else if (isForeignKeyConstraintError(
|
||||
serverMessage,
|
||||
config.getWebAuthNUsersTable(),
|
||||
new String[]{"user_id"},
|
||||
new Object[]{tenantIdentifier.getAppId()})) {
|
||||
throw new io.supertokens.pluginInterface.webauthn.exceptions.UserIdNotFoundException();
|
||||
} else if (isForeignKeyConstraintError(
|
||||
serverMessage,
|
||||
config.getTenantsTable(),
|
||||
new String[]{"app_id", "tenant_id"},
|
||||
new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
|
||||
throw new TenantOrAppNotFoundException(tenantIdentifier);
|
||||
} else if (isForeignKeyConstraintError(
|
||||
serverMessage,
|
||||
config.getTenantsTable(),
|
||||
new String[]{"app_id"},
|
||||
new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
|
||||
throw new TenantOrAppNotFoundException(tenantIdentifier);
|
||||
}
|
||||
}
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthNOptions saveGeneratedOptions(TenantIdentifier tenantIdentifier, WebAuthNOptions optionsToSave)
|
||||
throws StorageQueryException, DuplicateOptionsIdException, TenantOrAppNotFoundException {
|
||||
try {
|
||||
return WebAuthNQueries.saveOptions(this, tenantIdentifier, optionsToSave);
|
||||
} catch (SQLException e) {
|
||||
if (e instanceof SQLiteException) {
|
||||
SQLiteConfig config = Config.getConfig(this);
|
||||
String serverMessage = e.getMessage();
|
||||
|
||||
if (isPrimaryKeyError(serverMessage, config.getWebAuthNGeneratedOptionsTable(),
|
||||
new String[]{"app_id", "tenant_id", "id"})) {
|
||||
throw new DuplicateOptionsIdException();
|
||||
} else if (isForeignKeyConstraintError(
|
||||
serverMessage,
|
||||
config.getTenantsTable(),
|
||||
new String[]{"app_id", "tenant_id"},
|
||||
new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
|
||||
throw new TenantOrAppNotFoundException(tenantIdentifier);
|
||||
} else if (isForeignKeyConstraintError(
|
||||
serverMessage,
|
||||
config.getTenantsTable(),
|
||||
new String[]{"app_id"},
|
||||
new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
|
||||
throw new TenantOrAppNotFoundException(tenantIdentifier);
|
||||
}
|
||||
}
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthNOptions loadOptionsById(TenantIdentifier tenantIdentifier, String optionsId)
|
||||
throws StorageQueryException {
|
||||
try {
|
||||
return WebAuthNQueries.loadOptionsById(this, tenantIdentifier, optionsId);
|
||||
} catch (SQLException e){
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthNStoredCredential loadCredentialByIdForUser(TenantIdentifier tenantIdentifier, String credentialId, String recipeUserId)
|
||||
throws StorageQueryException {
|
||||
try {
|
||||
return WebAuthNQueries.loadCredentialByIdForUser(this, tenantIdentifier, credentialId, recipeUserId);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthNOptions loadOptionsById_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con,
|
||||
String optionsId) throws StorageQueryException {
|
||||
try {
|
||||
Connection sqlCon = (Connection) con.getConnection();
|
||||
return WebAuthNQueries.loadOptionsById_Transaction(this, sqlCon, tenantIdentifier, optionsId);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthNStoredCredential loadCredentialById_Transaction(TenantIdentifier tenantIdentifier,
|
||||
TransactionConnection con, String credentialId)
|
||||
throws StorageQueryException {
|
||||
try {
|
||||
Connection sqlCon = (Connection) con.getConnection();
|
||||
return WebAuthNQueries.loadCredentialById_Transaction(this, sqlCon, tenantIdentifier, credentialId);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthRecipeUserInfo signUpWithCredentialsRegister_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con,
|
||||
String userId, String email, String relyingPartyId, WebAuthNStoredCredential credential)
|
||||
throws StorageQueryException, io.supertokens.pluginInterface.webauthn.exceptions.DuplicateUserIdException, TenantOrAppNotFoundException,
|
||||
DuplicateUserEmailException {
|
||||
Connection sqlCon = (Connection) con.getConnection();
|
||||
try {
|
||||
return WebAuthNQueries.signUpWithCredentialRegister_Transaction(this, sqlCon, tenantIdentifier, userId, email, relyingPartyId, credential);
|
||||
} catch (StorageTransactionLogicException stle) {
|
||||
if (stle.actualException instanceof SQLiteException) {
|
||||
SQLiteConfig config = Config.getConfig(this);
|
||||
String serverMessage = stle.actualException.getMessage();
|
||||
|
||||
if (isUniqueConstraintError(serverMessage, config.getWebAuthNUserToTenantTable(),
|
||||
new String[]{"app_id", "tenant_id", "email"})) {
|
||||
throw new DuplicateUserEmailException();
|
||||
} else if (isPrimaryKeyError(serverMessage, config.getWebAuthNUsersTable(),
|
||||
new String[]{"app_id", "user_id"})
|
||||
|| isPrimaryKeyError(serverMessage, config.getUsersTable(),
|
||||
new String[]{"app_id", "tenant_id", "user_id"})
|
||||
|| isPrimaryKeyError(serverMessage, config.getWebAuthNUserToTenantTable(),
|
||||
new String[]{"app_id", "tenant_id", "user_id"})
|
||||
|| isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(),
|
||||
new String[]{"app_id", "user_id"})) {
|
||||
throw new io.supertokens.pluginInterface.webauthn.exceptions.DuplicateUserIdException();
|
||||
} else if (isForeignKeyConstraintError(
|
||||
serverMessage,
|
||||
config.getAppsTable(),
|
||||
new String[]{"app_id"},
|
||||
new Object[]{tenantIdentifier.getAppId()})) {
|
||||
throw new TenantOrAppNotFoundException(tenantIdentifier);
|
||||
} else if (isForeignKeyConstraintError(
|
||||
serverMessage,
|
||||
config.getTenantsTable(),
|
||||
new String[]{"app_id", "tenant_id"},
|
||||
new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
|
||||
throw new TenantOrAppNotFoundException(tenantIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
throw new StorageQueryException(stle.actualException);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthRecipeUserInfo signUp_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con,
|
||||
String userId, String email, String relyingPartyId)
|
||||
throws StorageQueryException, TenantOrAppNotFoundException, DuplicateUserEmailException,
|
||||
io.supertokens.pluginInterface.webauthn.exceptions.DuplicateUserIdException {
|
||||
Connection sqlCon = (Connection) con.getConnection();
|
||||
try {
|
||||
return WebAuthNQueries.signUp_Transaction(this, sqlCon, tenantIdentifier, userId, email, relyingPartyId);
|
||||
} catch (StorageTransactionLogicException stle) {
|
||||
if (stle.actualException instanceof SQLiteException) {
|
||||
SQLiteConfig config = Config.getConfig(this);
|
||||
String serverMessage = stle.actualException.getMessage();
|
||||
|
||||
if (isUniqueConstraintError(serverMessage, config.getWebAuthNUserToTenantTable(),
|
||||
new String[]{"app_id", "tenant_id", "email"})) {
|
||||
throw new DuplicateUserEmailException();
|
||||
} else if (isPrimaryKeyError(serverMessage, config.getWebAuthNUsersTable(),
|
||||
new String[]{"app_id", "user_id"})
|
||||
|| isPrimaryKeyError(serverMessage, config.getUsersTable(),
|
||||
new String[]{"app_id", "tenant_id", "user_id"})
|
||||
|| isPrimaryKeyError(serverMessage, config.getWebAuthNUserToTenantTable(),
|
||||
new String[]{"app_id", "tenant_id", "user_id"})
|
||||
|| isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(),
|
||||
new String[]{"app_id", "user_id"})) {
|
||||
throw new io.supertokens.pluginInterface.webauthn.exceptions.DuplicateUserIdException();
|
||||
} else if (isForeignKeyConstraintError(
|
||||
serverMessage,
|
||||
config.getAppsTable(),
|
||||
new String[]{"app_id"},
|
||||
new Object[]{tenantIdentifier.getAppId()})) {
|
||||
throw new TenantOrAppNotFoundException(tenantIdentifier);
|
||||
} else if (isForeignKeyConstraintError(
|
||||
serverMessage,
|
||||
config.getTenantsTable(),
|
||||
new String[]{"app_id", "tenant_id"},
|
||||
new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) {
|
||||
throw new TenantOrAppNotFoundException(tenantIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
throw new StorageQueryException(stle.actualException);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthRecipeUserInfo getUserInfoByCredentialId_Transaction(TenantIdentifier tenantIdentifier,
|
||||
TransactionConnection con, String credentialId)
|
||||
throws StorageQueryException {
|
||||
try {
|
||||
Connection sqlCon = (Connection) con.getConnection();
|
||||
return WebAuthNQueries.getUserInfoByCredentialId_Transaction(this, sqlCon, tenantIdentifier, credentialId);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCounter_Transaction(TenantIdentifier tenantIdentifier,
|
||||
TransactionConnection con, String credentialId,
|
||||
long counter) throws StorageQueryException {
|
||||
try {
|
||||
Connection sqlCon = (Connection) con.getConnection();
|
||||
WebAuthNQueries.updateCounter_Transaction(this, sqlCon, tenantIdentifier, credentialId, counter);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRecoverAccountToken(TenantIdentifier tenantIdentifier, AccountRecoveryTokenInfo accountRecoveryTokenInfo)
|
||||
throws DuplicateRecoverAccountTokenException, StorageQueryException {
|
||||
try {
|
||||
WebAuthNQueries.addRecoverAccountToken(this, tenantIdentifier, accountRecoveryTokenInfo);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCredential(TenantIdentifier tenantIdentifier, String userId, String credentialId)
|
||||
throws StorageQueryException, WebauthNCredentialNotExistsException {
|
||||
try {
|
||||
int rowsUpdated = WebAuthNQueries.removeCredential(this, tenantIdentifier, userId, credentialId);
|
||||
if(rowsUpdated < 1) {
|
||||
throw new WebauthNCredentialNotExistsException();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeOptions(TenantIdentifier tenantIdentifier, String optionsId)
|
||||
throws StorageQueryException, WebauthNOptionsNotExistsException {
|
||||
try {
|
||||
int rowsUpdated = WebAuthNQueries.removeOptions(this, tenantIdentifier, optionsId);
|
||||
if(rowsUpdated < 1) {
|
||||
throw new WebauthNOptionsNotExistsException();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WebAuthNStoredCredential> listCredentialsForUser(TenantIdentifier tenantIdentifier, String userId)
|
||||
throws StorageQueryException {
|
||||
try {
|
||||
return WebAuthNQueries.listCredentials(this, tenantIdentifier, userId);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUserEmail(TenantIdentifier tenantIdentifier, String userId, String newEmail)
|
||||
throws StorageQueryException, io.supertokens.pluginInterface.webauthn.exceptions.UserIdNotFoundException,
|
||||
DuplicateUserEmailException {
|
||||
try {
|
||||
WebAuthNQueries.updateUserEmail(this, tenantIdentifier, userId, newEmail);
|
||||
} catch (StorageQueryException e) {
|
||||
if (e.getCause() instanceof SQLiteException){
|
||||
String errorMessage = e.getCause().getMessage();
|
||||
SQLiteConfig config = Config.getConfig(this);
|
||||
|
||||
if (isUniqueConstraintError(errorMessage, config.getWebAuthNUserToTenantTable(),
|
||||
new String[]{"app_id", "tenant_id", "email"})) {
|
||||
throw new DuplicateUserEmailException();
|
||||
} else if (isForeignKeyConstraintError(
|
||||
errorMessage,
|
||||
config.getWebAuthNUserToTenantTable(),
|
||||
new String[]{"app_id", "tenant_id", "user_id"},
|
||||
new Object[]{tenantIdentifier.getAppId(), userId})) {
|
||||
throw new io.supertokens.pluginInterface.webauthn.exceptions.UserIdNotFoundException();
|
||||
}
|
||||
}
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUserEmail_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId,
|
||||
String newEmail)
|
||||
throws StorageQueryException, io.supertokens.pluginInterface.webauthn.exceptions.UserIdNotFoundException,
|
||||
DuplicateUserEmailException {
|
||||
try {
|
||||
Connection sqlCon = (Connection) con.getConnection();
|
||||
WebAuthNQueries.updateUserEmail_Transaction(this, sqlCon, tenantIdentifier, userId, newEmail);
|
||||
} catch (StorageQueryException e) {
|
||||
if (e.getCause() instanceof SQLiteException){
|
||||
String errorMessage = e.getCause().getMessage();
|
||||
SQLiteConfig config = Config.getConfig(this);
|
||||
|
||||
if (isUniqueConstraintError(errorMessage, config.getWebAuthNUserToTenantTable(),
|
||||
new String[]{"app_id", "tenant_id", "email"})) {
|
||||
throw new DuplicateUserEmailException();
|
||||
} else if (isForeignKeyConstraintError(
|
||||
errorMessage,
|
||||
config.getWebAuthNUserToTenantTable(),
|
||||
new String[]{"app_id", "tenant_id", "user_id"},
|
||||
new Object[]{tenantIdentifier.getAppId(), userId})) {
|
||||
throw new io.supertokens.pluginInterface.webauthn.exceptions.UserIdNotFoundException();
|
||||
}
|
||||
}
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountRecoveryTokenInfo getAccountRecoveryTokenInfoByToken_Transaction(TenantIdentifier tenantIdentifier,
|
||||
TransactionConnection con,
|
||||
String token)
|
||||
throws StorageQueryException {
|
||||
|
||||
Connection sqlCon = (Connection) con.getConnection();
|
||||
try {
|
||||
return WebAuthNQueries.getAccountRecoveryTokenInfoByToken_Transaction(this, tenantIdentifier, sqlCon, token);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAccountRecoveryTokenByEmail_Transaction(TenantIdentifier tenantIdentifier,
|
||||
TransactionConnection con, String email)
|
||||
throws StorageQueryException {
|
||||
Connection sqlCon = (Connection) con.getConnection();
|
||||
try {
|
||||
WebAuthNQueries.deleteAccountRecoveryTokenByEmail_Transaction(this, sqlCon, tenantIdentifier, email);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteExpiredAccountRecoveryTokens() throws StorageQueryException {
|
||||
try {
|
||||
WebAuthNQueries.deleteExpiredAccountRecoveryTokens(this);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteExpiredGeneratedOptions() throws StorageQueryException {
|
||||
try {
|
||||
WebAuthNQueries.deleteExpiredGeneratedOptions(this);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SAMLClient createOrUpdateSAMLClient(TenantIdentifier tenantIdentifier, SAMLClient samlClient)
|
||||
throws StorageQueryException, io.supertokens.pluginInterface.saml.exception.DuplicateEntityIdException {
|
||||
try {
|
||||
return SAMLQueries.createOrUpdateSAMLClient(this, tenantIdentifier, samlClient.clientId, samlClient.clientSecret,
|
||||
samlClient.ssoLoginURL, samlClient.redirectURIs.toString(), samlClient.defaultRedirectURI,
|
||||
samlClient.idpEntityId, samlClient.idpSigningCertificate, samlClient.allowIDPInitiatedLogin,
|
||||
samlClient.enableRequestSigning);
|
||||
} catch (SQLException e) {
|
||||
String errorMessage = e.getMessage();
|
||||
String table = io.supertokens.inmemorydb.config.Config.getConfig(this).getSAMLClientsTable();
|
||||
if (isUniqueConstraintError(errorMessage, table, new String[]{"app_id", "tenant_id", "idp_entity_id"})) {
|
||||
throw new io.supertokens.pluginInterface.saml.exception.DuplicateEntityIdException();
|
||||
}
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeSAMLClient(TenantIdentifier tenantIdentifier, String clientId) throws StorageQueryException {
|
||||
return SAMLQueries.removeSAMLClient(this, tenantIdentifier, clientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SAMLClient getSAMLClient(TenantIdentifier tenantIdentifier, String clientId) throws StorageQueryException {
|
||||
return SAMLQueries.getSAMLClient(this, tenantIdentifier, clientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SAMLClient getSAMLClientByIDPEntityId(TenantIdentifier tenantIdentifier, String idpEntityId) throws StorageQueryException {
|
||||
return SAMLQueries.getSAMLClientByIDPEntityId(this, tenantIdentifier, idpEntityId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SAMLClient> getSAMLClients(TenantIdentifier tenantIdentifier) throws StorageQueryException {
|
||||
return SAMLQueries.getSAMLClients(this, tenantIdentifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveRelayStateInfo(TenantIdentifier tenantIdentifier, SAMLRelayStateInfo relayStateInfo, long relayStateValidity) throws StorageQueryException {
|
||||
SAMLQueries.saveRelayStateInfo(this, tenantIdentifier, relayStateInfo.relayState, relayStateInfo.clientId, relayStateInfo.state, relayStateInfo.redirectURI, relayStateValidity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SAMLRelayStateInfo getRelayStateInfo(TenantIdentifier tenantIdentifier, String relayState) throws StorageQueryException {
|
||||
return SAMLQueries.getRelayStateInfo(this, tenantIdentifier, relayState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveSAMLClaims(TenantIdentifier tenantIdentifier, String clientId, String code, JsonObject claims, long claimsValidity) throws StorageQueryException {
|
||||
SAMLQueries.saveSAMLClaims(this, tenantIdentifier, clientId, code, claims.toString(), claimsValidity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SAMLClaimsInfo getSAMLClaimsAndRemoveCode(TenantIdentifier tenantIdentifier, String code) throws StorageQueryException {
|
||||
return SAMLQueries.getSAMLClaimsAndRemoveCode(this, tenantIdentifier, code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeExpiredSAMLCodesAndRelayStates() throws StorageQueryException {
|
||||
SAMLQueries.removeExpiredSAMLCodesAndRelayStates(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countSAMLClients(TenantIdentifier tenantIdentifier) throws StorageQueryException {
|
||||
return SAMLQueries.countSAMLClients(this, tenantIdentifier);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,4 +184,20 @@ public class SQLiteConfig {
|
|||
public String getOAuthLogoutChallengesTable() {
|
||||
return "oauth_logout_challenges";
|
||||
}
|
||||
|
||||
public String getWebAuthNUsersTable(){ return "webauthn_users";}
|
||||
|
||||
public String getWebAuthNUserToTenantTable(){ return "webauthn_user_to_tenant"; }
|
||||
|
||||
public String getWebAuthNGeneratedOptionsTable() { return "webauthn_generated_options"; }
|
||||
|
||||
public String getWebAuthNCredentialsTable() { return "webauthn_credentials"; }
|
||||
|
||||
public String getWebAuthNAccountRecoveryTokenTable() { return "webauthn_account_recovery_tokens"; }
|
||||
|
||||
public String getSAMLClientsTable() { return "saml_clients"; }
|
||||
|
||||
public String getSAMLRelayStateTable() { return "saml_relay_state"; }
|
||||
|
||||
public String getSAMLClaimsTable() { return "saml_claims"; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ public class EmailVerificationQueries {
|
|||
+ ");";
|
||||
}
|
||||
|
||||
static String getQueryToCreateEmailVerificationVerifiedEmailsAppIdIndex(Start start) {
|
||||
return "CREATE INDEX emailverification_verified_emails_verified_appid_emails_index ON "
|
||||
+ Config.getConfig(start).getEmailVerificationTable() + "(app_id, email);";
|
||||
}
|
||||
|
||||
static String getQueryToCreateEmailVerificationTokensTable(Start start) {
|
||||
return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getEmailVerificationTokensTable() + " ("
|
||||
+ "app_id VARCHAR(64) DEFAULT 'public',"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
|
|||
import io.supertokens.pluginInterface.authRecipe.LoginMethod;
|
||||
import io.supertokens.pluginInterface.dashboard.DashboardSearchTags;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
|
||||
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
|
||||
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -317,6 +318,9 @@ public class GeneralQueries {
|
|||
if (!doesTableExists(start, Config.getConfig(start).getEmailVerificationTable())) {
|
||||
getInstance(main).addState(CREATING_NEW_TABLE, null);
|
||||
update(start, getQueryToCreateEmailVerificationTable(start), NO_OP_SETTER);
|
||||
|
||||
//index
|
||||
update(start, getQueryToCreateEmailVerificationVerifiedEmailsAppIdIndex(start), NO_OP_SETTER);
|
||||
}
|
||||
|
||||
if (!doesTableExists(start, Config.getConfig(start).getEmailVerificationTokensTable())) {
|
||||
|
|
@ -406,6 +410,7 @@ public class GeneralQueries {
|
|||
|
||||
// index
|
||||
update(start, UserRolesQueries.getQueryToCreateUserRolesRoleIndex(start), NO_OP_SETTER);
|
||||
update(start, UserRolesQueries.getQueryToCreateUserRolesUserIdAppIdIndex(start), NO_OP_SETTER);
|
||||
}
|
||||
|
||||
if (!doesTableExists(start, Config.getConfig(start).getUserIdMappingTable())) {
|
||||
|
|
@ -473,6 +478,71 @@ public class GeneralQueries {
|
|||
// index
|
||||
update(start, OAuthQueries.getQueryToCreateOAuthLogoutChallengesTimeCreatedIndex(start), NO_OP_SETTER);
|
||||
}
|
||||
|
||||
if(!doesTableExists(start, Config.getConfig(start).getWebAuthNUsersTable())){
|
||||
getInstance(main).addState(CREATING_NEW_TABLE, null);
|
||||
update(start, WebAuthNQueries.getQueryToCreateWebAuthNUsersTable(start), NO_OP_SETTER);
|
||||
}
|
||||
|
||||
if(!doesTableExists(start, Config.getConfig(start).getWebAuthNUserToTenantTable())){
|
||||
getInstance(main).addState(CREATING_NEW_TABLE, null);
|
||||
update(start, WebAuthNQueries.getQueryToCreateWebAuthNUsersToTenantTable(start), NO_OP_SETTER);
|
||||
|
||||
//index
|
||||
update(start, WebAuthNQueries.getQueryToCreateWebAuthNUserToTenantEmailIndex(start), NO_OP_SETTER);
|
||||
}
|
||||
|
||||
if(!doesTableExists(start, Config.getConfig(start).getWebAuthNGeneratedOptionsTable())){
|
||||
getInstance(main).addState(CREATING_NEW_TABLE, null);
|
||||
update(start, WebAuthNQueries.getQueryToCreateWebAuthNGeneratedOptionsTable(start), NO_OP_SETTER);
|
||||
//index
|
||||
update(start, WebAuthNQueries.getQueryToCreateWebAuthNChallengeExpiresIndex(start), NO_OP_SETTER);
|
||||
}
|
||||
|
||||
if(!doesTableExists(start, Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable())){
|
||||
getInstance(main).addState(CREATING_NEW_TABLE, null);
|
||||
update(start, WebAuthNQueries.getQueryToCreateWebAuthNAccountRecoveryTokenTable(start), NO_OP_SETTER);
|
||||
|
||||
//index
|
||||
update(start, WebAuthNQueries.getQueryToCreateWebAuthNAccountRecoveryTokenTokenIndex(start), NO_OP_SETTER);
|
||||
update(start, WebAuthNQueries.getQueryToCreateWebAuthNAccountRecoveryTokenEmailIndex(start), NO_OP_SETTER);
|
||||
update(start, WebAuthNQueries.getQueryToCreateWebAuthNAccountRecoveryTokenExpiresAtIndex(start), NO_OP_SETTER);
|
||||
}
|
||||
|
||||
if(!doesTableExists(start, Config.getConfig(start).getWebAuthNCredentialsTable())){
|
||||
getInstance(main).addState(CREATING_NEW_TABLE, null);
|
||||
update(start, WebAuthNQueries.getQueryToCreateWebAuthNCredentialsTable(start), NO_OP_SETTER);
|
||||
|
||||
//index
|
||||
update(start, WebAuthNQueries.getQueryToCreateWebAuthNCredentialsUserIdIndex(start), NO_OP_SETTER);
|
||||
}
|
||||
|
||||
// SAML tables
|
||||
if (!doesTableExists(start, Config.getConfig(start).getSAMLClientsTable())) {
|
||||
getInstance(main).addState(CREATING_NEW_TABLE, null);
|
||||
update(start, SAMLQueries.getQueryToCreateSAMLClientsTable(start), NO_OP_SETTER);
|
||||
|
||||
// indexes
|
||||
update(start, SAMLQueries.getQueryToCreateSAMLClientsAppIdTenantIdIndex(start), NO_OP_SETTER);
|
||||
}
|
||||
|
||||
if (!doesTableExists(start, Config.getConfig(start).getSAMLRelayStateTable())) {
|
||||
getInstance(main).addState(CREATING_NEW_TABLE, null);
|
||||
update(start, SAMLQueries.getQueryToCreateSAMLRelayStateTable(start), NO_OP_SETTER);
|
||||
|
||||
// indexes
|
||||
update(start, SAMLQueries.getQueryToCreateSAMLRelayStateAppIdTenantIdIndex(start), NO_OP_SETTER);
|
||||
update(start, SAMLQueries.getQueryToCreateSAMLRelayStateExpiresAtIndex(start), NO_OP_SETTER);
|
||||
}
|
||||
|
||||
if (!doesTableExists(start, Config.getConfig(start).getSAMLClaimsTable())) {
|
||||
getInstance(main).addState(CREATING_NEW_TABLE, null);
|
||||
update(start, SAMLQueries.getQueryToCreateSAMLClaimsTable(start), NO_OP_SETTER);
|
||||
|
||||
// indexes
|
||||
update(start, SAMLQueries.getQueryToCreateSAMLClaimsAppIdTenantIdIndex(start), NO_OP_SETTER);
|
||||
update(start, SAMLQueries.getQueryToCreateSAMLClaimsExpiresAtIndex(start), NO_OP_SETTER);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier,
|
||||
|
|
@ -831,6 +901,45 @@ public class GeneralQueries {
|
|||
}
|
||||
}
|
||||
|
||||
{
|
||||
// check if we should search through the webauthn table
|
||||
if (dashboardSearchTags.shouldWebauthnTableBeSearched()) {
|
||||
String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable()
|
||||
+ " AS allAuthUsersTable" +
|
||||
" JOIN " + getConfig(start).getWebAuthNUserToTenantTable()
|
||||
+ " AS webauthnTable ON allAuthUsersTable.app_id = webauthnTable.app_id AND "
|
||||
+ "allAuthUsersTable.tenant_id = webauthnTable.tenant_id AND "
|
||||
+ "allAuthUsersTable.user_id = webauthnTable.user_id";
|
||||
|
||||
// attach email tags to queries
|
||||
QUERY = QUERY +
|
||||
" WHERE (webauthnTable.app_id = ? AND webauthnTable.tenant_id = ?) AND"
|
||||
+ " ( webauthnTable.email LIKE ? OR webauthnTable.email LIKE ? ";
|
||||
queryList.add(tenantIdentifier.getAppId());
|
||||
queryList.add(tenantIdentifier.getTenantId());
|
||||
queryList.add(dashboardSearchTags.emails.get(0) + "%");
|
||||
queryList.add("%@" + dashboardSearchTags.emails.get(0) + "%");
|
||||
for (int i = 1; i < dashboardSearchTags.emails.size(); i++) {
|
||||
QUERY += " OR webauthnTable.email LIKE ? OR webauthnTable.email LIKE ?";
|
||||
queryList.add(dashboardSearchTags.emails.get(i) + "%");
|
||||
queryList.add("%@" + dashboardSearchTags.emails.get(i) + "%");
|
||||
}
|
||||
|
||||
QUERY += " )";
|
||||
|
||||
// check if we need to append this to an existing search query
|
||||
if (USER_SEARCH_TAG_CONDITION.length() != 0) {
|
||||
USER_SEARCH_TAG_CONDITION.append(" UNION ").append("SELECT * FROM ( ").append(QUERY)
|
||||
.append(" LIMIT 1000) AS webauthnResultTable");
|
||||
|
||||
} else {
|
||||
USER_SEARCH_TAG_CONDITION.append("SELECT * FROM ( ").append(QUERY)
|
||||
.append(" LIMIT 1000) AS webauthnResultTable");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (USER_SEARCH_TAG_CONDITION.toString().length() == 0) {
|
||||
usersFromQuery = new ArrayList<>();
|
||||
} else {
|
||||
|
|
@ -1193,6 +1302,13 @@ public class GeneralQueries {
|
|||
|
||||
userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail_Transaction(start, sqlCon, appIdentifier, email));
|
||||
|
||||
String webauthnUserId = WebAuthNQueries.getPrimaryUserIdForAppUsingEmail_Transaction(start, sqlCon,
|
||||
appIdentifier, email);
|
||||
if(webauthnUserId != null) {
|
||||
userIds.add(webauthnUserId);
|
||||
}
|
||||
|
||||
|
||||
// remove duplicates from userIds
|
||||
Set<String> userIdsSet = new HashSet<>(userIds);
|
||||
userIds = new ArrayList<>(userIdsSet);
|
||||
|
|
@ -1224,6 +1340,11 @@ public class GeneralQueries {
|
|||
|
||||
userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier, email));
|
||||
|
||||
String webauthnUserId = WebAuthNQueries.getPrimaryUserIdForTenantUsingEmail(start, tenantIdentifier, email);
|
||||
if(webauthnUserId != null) {
|
||||
userIds.add(webauthnUserId);
|
||||
}
|
||||
|
||||
// remove duplicates from userIds
|
||||
Set<String> userIdsSet = new HashSet<>(userIds);
|
||||
userIds = new ArrayList<>(userIdsSet);
|
||||
|
|
@ -1268,6 +1389,34 @@ public class GeneralQueries {
|
|||
return getPrimaryUserInfoForUserId(start, tenantIdentifier.toAppIdentifier(), userId);
|
||||
}
|
||||
|
||||
public static AuthRecipeUserInfo getPrimaryUserByWebauthNCredentialId(Start start,
|
||||
TenantIdentifier tenantIdentifier,
|
||||
String credentialId)
|
||||
throws StorageQueryException, SQLException, StorageTransactionLogicException {
|
||||
AuthRecipeUserInfo webauthnUser = start.startTransaction(con -> {
|
||||
try {
|
||||
Connection sqlCon = (Connection) con.getConnection();
|
||||
return getPrimaryUserByWebauthNCredentialId_Transaction(start, sqlCon, tenantIdentifier,
|
||||
credentialId);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
});
|
||||
return webauthnUser;
|
||||
}
|
||||
|
||||
public static AuthRecipeUserInfo getPrimaryUserByWebauthNCredentialId_Transaction(Start start,
|
||||
Connection connection,
|
||||
TenantIdentifier tenantIdentifier,
|
||||
String credentialId)
|
||||
throws StorageQueryException, SQLException, StorageTransactionLogicException {
|
||||
|
||||
AuthRecipeUserInfo webauthnUser = WebAuthNQueries.getUserInfoByCredentialId_Transaction(start, connection,
|
||||
tenantIdentifier, credentialId);
|
||||
return getPrimaryUserInfoForUserId_Transaction(start, connection, tenantIdentifier.toAppIdentifier(),
|
||||
webauthnUser.getSupertokensUserId());
|
||||
}
|
||||
|
||||
public static String getPrimaryUserIdStrForUserId(Start start, AppIdentifier appIdentifier, String id)
|
||||
throws SQLException, StorageQueryException {
|
||||
String QUERY = "SELECT primary_or_recipe_user_id FROM " + getConfig(start).getUsersTable() +
|
||||
|
|
@ -1381,6 +1530,7 @@ public class GeneralQueries {
|
|||
loginMethods.addAll(ThirdPartyQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier));
|
||||
loginMethods.addAll(
|
||||
PasswordlessQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier));
|
||||
loginMethods.addAll(WebAuthNQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier));
|
||||
|
||||
Map<String, LoginMethod> recipeUserIdToLoginMethodMap = new HashMap<>();
|
||||
for (LoginMethod loginMethod : loginMethods) {
|
||||
|
|
@ -1480,6 +1630,8 @@ public class GeneralQueries {
|
|||
loginMethods.addAll(
|
||||
PasswordlessQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch,
|
||||
appIdentifier));
|
||||
loginMethods.addAll(WebAuthNQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch,
|
||||
appIdentifier));
|
||||
|
||||
Map<String, LoginMethod> recipeUserIdToLoginMethodMap = new HashMap<>();
|
||||
for (LoginMethod loginMethod : loginMethods) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,458 @@
|
|||
/*
|
||||
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0 (the
|
||||
* "License") as published by the Apache Software Foundation.
|
||||
*
|
||||
* You may not use this file except in compliance with the License. You may
|
||||
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package io.supertokens.inmemorydb.queries;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
|
||||
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
|
||||
import io.supertokens.inmemorydb.Start;
|
||||
import io.supertokens.inmemorydb.config.Config;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
|
||||
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
||||
import io.supertokens.pluginInterface.saml.SAMLClaimsInfo;
|
||||
import io.supertokens.pluginInterface.saml.SAMLClient;
|
||||
import io.supertokens.pluginInterface.saml.SAMLRelayStateInfo;
|
||||
|
||||
public class SAMLQueries {
|
||||
public static String getQueryToCreateSAMLClientsTable(Start start) {
|
||||
String table = Config.getConfig(start).getSAMLClientsTable();
|
||||
String tenantsTable = Config.getConfig(start).getTenantsTable();
|
||||
// @formatter:off
|
||||
return "CREATE TABLE IF NOT EXISTS " + table + " ("
|
||||
+ "app_id VARCHAR(64) NOT NULL DEFAULT 'public',"
|
||||
+ "tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',"
|
||||
+ "client_id VARCHAR(255) NOT NULL,"
|
||||
+ "client_secret TEXT,"
|
||||
+ "sso_login_url TEXT NOT NULL,"
|
||||
+ "redirect_uris TEXT NOT NULL," // store JsonArray.toString()
|
||||
+ "default_redirect_uri VARCHAR(1024) NOT NULL,"
|
||||
+ "idp_entity_id VARCHAR(1024),"
|
||||
+ "idp_signing_certificate TEXT,"
|
||||
+ "allow_idp_initiated_login BOOLEAN NOT NULL DEFAULT FALSE,"
|
||||
+ "enable_request_signing BOOLEAN NOT NULL DEFAULT TRUE,"
|
||||
+ "created_at BIGINT NOT NULL,"
|
||||
+ "updated_at BIGINT NOT NULL,"
|
||||
+ "UNIQUE (app_id, tenant_id, idp_entity_id),"
|
||||
+ "PRIMARY KEY (app_id, tenant_id, client_id),"
|
||||
+ "FOREIGN KEY (app_id, tenant_id) REFERENCES " + tenantsTable + " (app_id, tenant_id) ON DELETE CASCADE"
|
||||
+ ");";
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
public static String getQueryToCreateSAMLClientsAppIdTenantIdIndex(Start start) {
|
||||
String table = Config.getConfig(start).getSAMLClientsTable();
|
||||
return "CREATE INDEX IF NOT EXISTS saml_clients_app_tenant_index ON " + table + "(app_id, tenant_id);";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateSAMLRelayStateTable(Start start) {
|
||||
String table = Config.getConfig(start).getSAMLRelayStateTable();
|
||||
String tenantsTable = Config.getConfig(start).getTenantsTable();
|
||||
// @formatter:off
|
||||
return "CREATE TABLE IF NOT EXISTS " + table + " ("
|
||||
+ "app_id VARCHAR(64) NOT NULL DEFAULT 'public',"
|
||||
+ "tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',"
|
||||
+ "relay_state VARCHAR(255) NOT NULL,"
|
||||
+ "client_id VARCHAR(255) NOT NULL,"
|
||||
+ "state TEXT,"
|
||||
+ "redirect_uri VARCHAR(1024) NOT NULL,"
|
||||
+ "created_at BIGINT NOT NULL,"
|
||||
+ "expires_at BIGINT NOT NULL,"
|
||||
+ "PRIMARY KEY (relay_state)," // relayState must be unique
|
||||
+ "FOREIGN KEY (app_id, tenant_id) REFERENCES " + tenantsTable + " (app_id, tenant_id) ON DELETE CASCADE"
|
||||
+ ");";
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
public static String getQueryToCreateSAMLRelayStateAppIdTenantIdIndex(Start start) {
|
||||
String table = Config.getConfig(start).getSAMLRelayStateTable();
|
||||
return "CREATE INDEX IF NOT EXISTS saml_relay_state_app_tenant_index ON " + table + "(app_id, tenant_id);";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateSAMLRelayStateExpiresAtIndex(Start start) {
|
||||
String table = Config.getConfig(start).getSAMLRelayStateTable();
|
||||
return "CREATE INDEX IF NOT EXISTS saml_relay_state_expires_at_index ON " + table + "(expires_at);";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateSAMLClaimsTable(Start start) {
|
||||
String table = Config.getConfig(start).getSAMLClaimsTable();
|
||||
String tenantsTable = Config.getConfig(start).getTenantsTable();
|
||||
// @formatter:off
|
||||
return "CREATE TABLE IF NOT EXISTS " + table + " ("
|
||||
+ "app_id VARCHAR(64) NOT NULL DEFAULT 'public',"
|
||||
+ "tenant_id VARCHAR(64) NOT NULL DEFAULT 'public',"
|
||||
+ "client_id VARCHAR(255) NOT NULL,"
|
||||
+ "code VARCHAR(255) NOT NULL,"
|
||||
+ "claims TEXT NOT NULL,"
|
||||
+ "created_at BIGINT NOT NULL,"
|
||||
+ "expires_at BIGINT NOT NULL,"
|
||||
+ "PRIMARY KEY (code),"
|
||||
+ "FOREIGN KEY (app_id, tenant_id) REFERENCES " + tenantsTable + " (app_id, tenant_id) ON DELETE CASCADE"
|
||||
+ ");";
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
public static String getQueryToCreateSAMLClaimsAppIdTenantIdIndex(Start start) {
|
||||
String table = Config.getConfig(start).getSAMLClaimsTable();
|
||||
return "CREATE INDEX IF NOT EXISTS saml_claims_app_tenant_index ON " + table + "(app_id, tenant_id);";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateSAMLClaimsExpiresAtIndex(Start start) {
|
||||
String table = Config.getConfig(start).getSAMLClaimsTable();
|
||||
return "CREATE INDEX IF NOT EXISTS saml_claims_expires_at_index ON " + table + "(expires_at);";
|
||||
}
|
||||
|
||||
public static void saveRelayStateInfo(Start start, TenantIdentifier tenantIdentifier,
|
||||
String relayState, String clientId, String state, String redirectURI, long relayStateValidity)
|
||||
throws StorageQueryException {
|
||||
String table = Config.getConfig(start).getSAMLRelayStateTable();
|
||||
String QUERY = "INSERT INTO " + table +
|
||||
" (app_id, tenant_id, relay_state, client_id, state, redirect_uri, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
try {
|
||||
update(start, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, relayState);
|
||||
pst.setString(4, clientId);
|
||||
if (state != null) {
|
||||
pst.setString(5, state);
|
||||
} else {
|
||||
pst.setNull(5, java.sql.Types.VARCHAR);
|
||||
}
|
||||
pst.setString(6, redirectURI);
|
||||
pst.setLong(7, System.currentTimeMillis());
|
||||
pst.setLong(8, System.currentTimeMillis() + relayStateValidity);
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static SAMLRelayStateInfo getRelayStateInfo(Start start, TenantIdentifier tenantIdentifier, String relayState)
|
||||
throws StorageQueryException {
|
||||
String table = Config.getConfig(start).getSAMLRelayStateTable();
|
||||
String QUERY = "SELECT client_id, state, redirect_uri, expires_at FROM " + table
|
||||
+ " WHERE app_id = ? AND tenant_id = ? AND relay_state = ? AND expires_at >= ?";
|
||||
|
||||
try {
|
||||
return execute(start, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, relayState);
|
||||
pst.setLong(4, System.currentTimeMillis());
|
||||
}, result -> {
|
||||
if (result.next()) {
|
||||
String clientId = result.getString("client_id");
|
||||
String state = result.getString("state"); // may be null
|
||||
String redirectURI = result.getString("redirect_uri");
|
||||
return new SAMLRelayStateInfo(relayState, clientId, state, redirectURI);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveSAMLClaims(Start start, TenantIdentifier tenantIdentifier, String clientId, String code, String claimsJson, long claimsValidity)
|
||||
throws StorageQueryException {
|
||||
String table = Config.getConfig(start).getSAMLClaimsTable();
|
||||
String QUERY = "INSERT INTO " + table +
|
||||
" (app_id, tenant_id, client_id, code, claims, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
try {
|
||||
update(start, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, clientId);
|
||||
pst.setString(4, code);
|
||||
pst.setString(5, claimsJson);
|
||||
pst.setLong(6, System.currentTimeMillis());
|
||||
pst.setLong(7, System.currentTimeMillis() + claimsValidity);
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static SAMLClaimsInfo getSAMLClaimsAndRemoveCode(Start start, TenantIdentifier tenantIdentifier, String code)
|
||||
throws StorageQueryException {
|
||||
String table = Config.getConfig(start).getSAMLClaimsTable();
|
||||
String QUERY = "SELECT client_id, claims FROM " + table + " WHERE app_id = ? AND tenant_id = ? AND code = ? AND expires_at >= ?";
|
||||
try {
|
||||
SAMLClaimsInfo claimsInfo = execute(start, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, code);
|
||||
pst.setLong(4, System.currentTimeMillis());
|
||||
}, result -> {
|
||||
if (result.next()) {
|
||||
String clientId = result.getString("client_id");
|
||||
JsonObject claims = com.google.gson.JsonParser.parseString(result.getString("claims")).getAsJsonObject();
|
||||
return new SAMLClaimsInfo(clientId, claims);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (claimsInfo != null) {
|
||||
String DELETE = "DELETE FROM " + table + " WHERE app_id = ? AND tenant_id = ? AND code = ?";
|
||||
update(start, DELETE, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, code);
|
||||
});
|
||||
}
|
||||
return claimsInfo;
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static SAMLClient createOrUpdateSAMLClient(
|
||||
Start start,
|
||||
TenantIdentifier tenantIdentifier,
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
String ssoLoginURL,
|
||||
String redirectURIsJson,
|
||||
String defaultRedirectURI,
|
||||
String idpEntityId,
|
||||
String idpSigningCertificate,
|
||||
boolean allowIDPInitiatedLogin,
|
||||
boolean enableRequestSigning)
|
||||
throws StorageQueryException, SQLException {
|
||||
String table = Config.getConfig(start).getSAMLClientsTable();
|
||||
String QUERY = "INSERT INTO " + table +
|
||||
" (app_id, tenant_id, client_id, client_secret, sso_login_url, redirect_uris, default_redirect_uri, idp_entity_id, idp_signing_certificate, allow_idp_initiated_login, enable_request_signing, created_at, updated_at) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " +
|
||||
"ON CONFLICT (app_id, tenant_id, client_id) DO UPDATE SET " +
|
||||
"client_secret = ?, sso_login_url = ?, redirect_uris = ?, default_redirect_uri = ?, idp_entity_id = ?, idp_signing_certificate = ?, allow_idp_initiated_login = ?, enable_request_signing = ?, updated_at = ?";
|
||||
long now = System.currentTimeMillis();
|
||||
update(start, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, clientId);
|
||||
if (clientSecret != null) {
|
||||
pst.setString(4, clientSecret);
|
||||
} else {
|
||||
pst.setNull(4, Types.VARCHAR);
|
||||
}
|
||||
pst.setString(5, ssoLoginURL);
|
||||
pst.setString(6, redirectURIsJson);
|
||||
pst.setString(7, defaultRedirectURI);
|
||||
if (idpEntityId != null) {
|
||||
pst.setString(8, idpEntityId);
|
||||
} else {
|
||||
pst.setNull(8, java.sql.Types.VARCHAR);
|
||||
}
|
||||
if (idpSigningCertificate != null) {
|
||||
pst.setString(9, idpSigningCertificate);
|
||||
} else {
|
||||
pst.setNull(9, Types.VARCHAR);
|
||||
}
|
||||
pst.setBoolean(10, allowIDPInitiatedLogin);
|
||||
pst.setBoolean(11, enableRequestSigning);
|
||||
pst.setLong(12, now);
|
||||
pst.setLong(13, now);
|
||||
|
||||
if (clientSecret != null) {
|
||||
pst.setString(14, clientSecret);
|
||||
} else {
|
||||
pst.setNull(14, Types.VARCHAR);
|
||||
}
|
||||
pst.setString(15, ssoLoginURL);
|
||||
pst.setString(16, redirectURIsJson);
|
||||
pst.setString(17, defaultRedirectURI);
|
||||
if (idpEntityId != null) {
|
||||
pst.setString(18, idpEntityId);
|
||||
} else {
|
||||
pst.setNull(18, java.sql.Types.VARCHAR);
|
||||
}
|
||||
if (idpSigningCertificate != null) {
|
||||
pst.setString(19, idpSigningCertificate);
|
||||
} else {
|
||||
pst.setNull(19, Types.VARCHAR);
|
||||
}
|
||||
pst.setBoolean(20, allowIDPInitiatedLogin);
|
||||
pst.setBoolean(21, enableRequestSigning);
|
||||
pst.setLong(22, now);
|
||||
});
|
||||
|
||||
return getSAMLClient(start, tenantIdentifier, clientId);
|
||||
}
|
||||
|
||||
public static SAMLClient getSAMLClient(Start start, TenantIdentifier tenantIdentifier, String clientId)
|
||||
throws StorageQueryException {
|
||||
String table = Config.getConfig(start).getSAMLClientsTable();
|
||||
String QUERY = "SELECT client_id, client_secret, sso_login_url, redirect_uris, default_redirect_uri, idp_entity_id, idp_signing_certificate, allow_idp_initiated_login, enable_request_signing FROM " + table
|
||||
+ " WHERE app_id = ? AND tenant_id = ? AND client_id = ?";
|
||||
|
||||
try {
|
||||
return execute(start, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, clientId);
|
||||
}, result -> {
|
||||
if (result.next()) {
|
||||
String fetchedClientId = result.getString("client_id");
|
||||
String clientSecret = result.getString("client_secret");
|
||||
String ssoLoginURL = result.getString("sso_login_url");
|
||||
String redirectUrisJson = result.getString("redirect_uris");
|
||||
String defaultRedirectURI = result.getString("default_redirect_uri");
|
||||
String idpEntityId = result.getString("idp_entity_id");
|
||||
String idpSigningCertificate = result.getString("idp_signing_certificate");
|
||||
boolean allowIDPInitiatedLogin = result.getBoolean("allow_idp_initiated_login");
|
||||
boolean enableRequestSigning = result.getBoolean("enable_request_signing");
|
||||
|
||||
JsonArray redirectURIs = JsonParser.parseString(redirectUrisJson).getAsJsonArray();
|
||||
return new SAMLClient(fetchedClientId, clientSecret, ssoLoginURL, redirectURIs, defaultRedirectURI, idpEntityId, idpSigningCertificate, allowIDPInitiatedLogin, enableRequestSigning);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static SAMLClient getSAMLClientByIDPEntityId(Start start, TenantIdentifier tenantIdentifier, String idpEntityId) throws StorageQueryException {
|
||||
String table = Config.getConfig(start).getSAMLClientsTable();
|
||||
String QUERY = "SELECT client_id, client_secret, sso_login_url, redirect_uris, default_redirect_uri, idp_entity_id, idp_signing_certificate, allow_idp_initiated_login, enable_request_signing FROM " + table
|
||||
+ " WHERE app_id = ? AND tenant_id = ? AND idp_entity_id = ?";
|
||||
|
||||
try {
|
||||
return execute(start, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, idpEntityId);
|
||||
}, result -> {
|
||||
if (result.next()) {
|
||||
String fetchedClientId = result.getString("client_id");
|
||||
String clientSecret = result.getString("client_secret");
|
||||
String ssoLoginURL = result.getString("sso_login_url");
|
||||
String redirectUrisJson = result.getString("redirect_uris");
|
||||
String defaultRedirectURI = result.getString("default_redirect_uri");
|
||||
String fetchedIdpEntityId = result.getString("idp_entity_id");
|
||||
String idpSigningCertificate = result.getString("idp_signing_certificate");
|
||||
boolean allowIDPInitiatedLogin = result.getBoolean("allow_idp_initiated_login");
|
||||
boolean enableRequestSigning = result.getBoolean("enable_request_signing");
|
||||
|
||||
JsonArray redirectURIs = JsonParser.parseString(redirectUrisJson).getAsJsonArray();
|
||||
return new SAMLClient(fetchedClientId, clientSecret, ssoLoginURL, redirectURIs, defaultRedirectURI, fetchedIdpEntityId, idpSigningCertificate, allowIDPInitiatedLogin, enableRequestSigning);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<SAMLClient> getSAMLClients(Start start, TenantIdentifier tenantIdentifier)
|
||||
throws StorageQueryException {
|
||||
String table = Config.getConfig(start).getSAMLClientsTable();
|
||||
String QUERY = "SELECT client_id, client_secret, sso_login_url, redirect_uris, default_redirect_uri, idp_entity_id, idp_signing_certificate, allow_idp_initiated_login, enable_request_signing FROM " + table
|
||||
+ " WHERE app_id = ? AND tenant_id = ?";
|
||||
|
||||
try {
|
||||
return execute(start, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
}, result -> {
|
||||
List<SAMLClient> clients = new ArrayList<>();
|
||||
while (result.next()) {
|
||||
String fetchedClientId = result.getString("client_id");
|
||||
String clientSecret = result.getString("client_secret");
|
||||
String ssoLoginURL = result.getString("sso_login_url");
|
||||
String redirectUrisJson = result.getString("redirect_uris");
|
||||
String defaultRedirectURI = result.getString("default_redirect_uri");
|
||||
String idpEntityId = result.getString("idp_entity_id");
|
||||
String idpSigningCertificate = result.getString("idp_signing_certificate");
|
||||
boolean allowIDPInitiatedLogin = result.getBoolean("allow_idp_initiated_login");
|
||||
boolean enableRequestSigning = result.getBoolean("enable_request_signing");
|
||||
|
||||
JsonArray redirectURIs = JsonParser.parseString(redirectUrisJson).getAsJsonArray();
|
||||
clients.add(new SAMLClient(fetchedClientId, clientSecret, ssoLoginURL, redirectURIs, defaultRedirectURI, idpEntityId, idpSigningCertificate, allowIDPInitiatedLogin, enableRequestSigning));
|
||||
}
|
||||
return clients;
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean removeSAMLClient(Start start, TenantIdentifier tenantIdentifier, String clientId)
|
||||
throws StorageQueryException {
|
||||
String table = Config.getConfig(start).getSAMLClientsTable();
|
||||
String QUERY = "DELETE FROM " + table + " WHERE app_id = ? AND tenant_id = ? AND client_id = ?";
|
||||
try {
|
||||
return update(start, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, clientId);
|
||||
}) > 0;
|
||||
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeExpiredSAMLCodesAndRelayStates(Start start) throws StorageQueryException {
|
||||
try {
|
||||
{
|
||||
String QUERY = "DELETE FROM " + Config.getConfig(start).getSAMLClaimsTable() + " WHERE expires_at <= ?";
|
||||
update(start, QUERY, pst -> {
|
||||
pst.setLong(1, System.currentTimeMillis());
|
||||
});
|
||||
}
|
||||
{
|
||||
String QUERY = "DELETE FROM " + Config.getConfig(start).getSAMLRelayStateTable() + " WHERE expires_at <= ?";
|
||||
update(start, QUERY, pst -> {
|
||||
pst.setLong(1, System.currentTimeMillis());
|
||||
});
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static int countSAMLClients(Start start, TenantIdentifier tenantIdentifier) throws StorageQueryException {
|
||||
String table = Config.getConfig(start).getSAMLClientsTable();
|
||||
String QUERY = "SELECT COUNT(*) as c FROM " + table
|
||||
+ " WHERE app_id = ? AND tenant_id = ?";
|
||||
|
||||
try {
|
||||
return execute(start, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
}, result -> {
|
||||
if (result.next()) {
|
||||
return result.getInt("c");
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,11 @@ public class UserRolesQueries {
|
|||
+ Config.getConfig(start).getUserRolesPermissionsTable() + "(app_id, permission);";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateUserRolesUserIdAppIdIndex(Start start) {
|
||||
return "CREATE INDEX user_roles_app_id_user_id_index ON " + Config.getConfig(start).getUserRolesTable() +
|
||||
"(app_id, user_id)";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateUserRolesTable(Start start) {
|
||||
String tableName = Config.getConfig(start).getUserRolesTable();
|
||||
// @formatter:off
|
||||
|
|
|
|||
|
|
@ -0,0 +1,777 @@
|
|||
/*
|
||||
* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0 (the
|
||||
* "License") as published by the Apache Software Foundation.
|
||||
*
|
||||
* You may not use this file except in compliance with the License. You may
|
||||
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package io.supertokens.inmemorydb.queries;
|
||||
|
||||
import io.supertokens.inmemorydb.Start;
|
||||
import io.supertokens.inmemorydb.Utils;
|
||||
import io.supertokens.inmemorydb.config.Config;
|
||||
import io.supertokens.pluginInterface.RowMapper;
|
||||
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
|
||||
import io.supertokens.pluginInterface.authRecipe.LoginMethod;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
|
||||
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
|
||||
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
||||
import io.supertokens.pluginInterface.webauthn.AccountRecoveryTokenInfo;
|
||||
import io.supertokens.pluginInterface.webauthn.WebAuthNOptions;
|
||||
import io.supertokens.pluginInterface.webauthn.WebAuthNStoredCredential;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
|
||||
import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
|
||||
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
|
||||
import static io.supertokens.inmemorydb.config.Config.getConfig;
|
||||
import static io.supertokens.pluginInterface.RECIPE_ID.WEBAUTHN;
|
||||
|
||||
public class WebAuthNQueries {
|
||||
|
||||
public static String getQueryToCreateWebAuthNUsersTable(Start start){
|
||||
return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getWebAuthNUsersTable() + "(" +
|
||||
" app_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
|
||||
" user_id CHAR(36) NOT NULL," +
|
||||
" email VARCHAR(256) NOT NULL," +
|
||||
" rp_id VARCHAR(256) NOT NULL," +
|
||||
" time_joined BIGINT UNSIGNED NOT NULL," +
|
||||
" CONSTRAINT webauthn_users_pkey PRIMARY KEY (app_id, user_id), " +
|
||||
" CONSTRAINT webauthn_users_to_app_id_fkey " +
|
||||
" FOREIGN KEY (app_id, user_id) REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() +
|
||||
" (app_id, user_id) ON DELETE CASCADE " +
|
||||
");";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateWebAuthNUsersToTenantTable(Start start){
|
||||
return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getWebAuthNUserToTenantTable() +" (" +
|
||||
" app_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
|
||||
" tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
|
||||
" user_id CHAR(36) NOT NULL," +
|
||||
" email VARCHAR(256) NOT NULL," +
|
||||
" CONSTRAINT webauthn_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email)," +
|
||||
" CONSTRAINT webauthn_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id)," +
|
||||
" CONSTRAINT webauthn_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) " +
|
||||
" REFERENCES "+ Config.getConfig(start).getUsersTable()+" (app_id, tenant_id, user_id) on delete CASCADE" +
|
||||
");";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateWebAuthNUserToTenantEmailIndex(Start start) {
|
||||
return "CREATE INDEX webauthn_user_to_tenant_email_index ON " +
|
||||
Config.getConfig(start).getWebAuthNUserToTenantTable() +
|
||||
" (app_id, email);";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateWebAuthNGeneratedOptionsTable(Start start){
|
||||
return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable() + "(" +
|
||||
" app_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
|
||||
" tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
|
||||
" id CHAR(36) NOT NULL," +
|
||||
" challenge VARCHAR(256) NOT NULL," +
|
||||
" email VARCHAR(256)," +
|
||||
" rp_id VARCHAR(256) NOT NULL," +
|
||||
" rp_name VARCHAR(256) NOT NULL," +
|
||||
" origin VARCHAR(256) NOT NULL," +
|
||||
" expires_at BIGINT UNSIGNED NOT NULL," +
|
||||
" created_at BIGINT UNSIGNED NOT NULL," +
|
||||
" user_presence_required BOOLEAN DEFAULT FALSE NOT NULL," +
|
||||
" user_verification VARCHAR(12) DEFAULT `preferred` NOT NULL," +
|
||||
" CONSTRAINT webauthn_user_challenges_pkey PRIMARY KEY (app_id, tenant_id, id)," +
|
||||
" CONSTRAINT webauthn_user_challenges_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) " +
|
||||
" REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE" +
|
||||
");";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateWebAuthNChallengeExpiresIndex(Start start) {
|
||||
return "CREATE INDEX webauthn_user_challenges_expires_at_index ON " +
|
||||
Config.getConfig(start).getWebAuthNGeneratedOptionsTable() +
|
||||
" (app_id, tenant_id, expires_at);";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateWebAuthNCredentialsTable(Start start){
|
||||
return "CREATE TABLE IF NOT EXISTS "+ Config.getConfig(start).getWebAuthNCredentialsTable() + "(" +
|
||||
" id VARCHAR(256) NOT NULL," +
|
||||
" app_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
|
||||
" rp_id VARCHAR(256)," +
|
||||
" user_id CHAR(36)," +
|
||||
" counter BIGINT NOT NULL," +
|
||||
" public_key BLOB NOT NULL," + //planned as bytea, which is not supported by sqlite
|
||||
" transports TEXT NOT NULL," + // planned as TEXT[], which is not supported by sqlite
|
||||
" created_at BIGINT NOT NULL," +
|
||||
" updated_at BIGINT NOT NULL," +
|
||||
" CONSTRAINT webauthn_user_credentials_pkey PRIMARY KEY (app_id, rp_id, id)," +
|
||||
" CONSTRAINT webauthn_user_credentials_webauthn_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES " +
|
||||
Config.getConfig(start).getWebAuthNUsersTable() + " (app_id, user_id) ON DELETE CASCADE" +
|
||||
");";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateWebAuthNCredentialsUserIdIndex(Start start) {
|
||||
return "CREATE INDEX IF NOT EXISTS webauthn_credentials_user_id_index ON " +
|
||||
Config.getConfig(start).getWebAuthNCredentialsTable() +
|
||||
" (user_id);";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateWebAuthNAccountRecoveryTokenTable(Start start) {
|
||||
return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() + "(" +
|
||||
" app_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
|
||||
" tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL," +
|
||||
" user_id CHAR(36) NOT NULL," +
|
||||
" email VARCHAR(256) NOT NULL," +
|
||||
" token VARCHAR(256) NOT NULL," +
|
||||
" expires_at BIGINT UNSIGNED NOT NULL," +
|
||||
" CONSTRAINT webauthn_account_recovery_token_pkey PRIMARY KEY (app_id, tenant_id, user_id, token)," +
|
||||
" CONSTRAINT webauthn_account_recovery_token_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES " +
|
||||
Config.getConfig(start).getUsersTable() + " (app_id, tenant_id, user_id) ON DELETE CASCADE" +
|
||||
");";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateWebAuthNAccountRecoveryTokenTokenIndex(Start start) {
|
||||
return "CREATE INDEX webauthn_account_recovery_token_token_index ON " +
|
||||
Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() +
|
||||
" (app_id, tenant_id, token);";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateWebAuthNAccountRecoveryTokenEmailIndex(Start start) {
|
||||
return "CREATE INDEX webauthn_account_recovery_token_email_index ON " +
|
||||
Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() +
|
||||
" (app_id, tenant_id, email);";
|
||||
}
|
||||
|
||||
public static String getQueryToCreateWebAuthNAccountRecoveryTokenExpiresAtIndex(Start start) {
|
||||
return "CREATE INDEX webauthn_account_recovery_token_expires_at_index ON " +
|
||||
Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() +
|
||||
" (expires_at DESC);";
|
||||
}
|
||||
|
||||
public static WebAuthNOptions saveOptions(Start start, TenantIdentifier tenantIdentifier, WebAuthNOptions options)
|
||||
throws SQLException, StorageQueryException {
|
||||
String INSERT = "INSERT INTO " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable()
|
||||
+ " (app_id, tenant_id, id, challenge, email, rp_id, origin, expires_at, created_at, rp_name, user_verification, user_presence_required) "
|
||||
+ " VALUES (?,?,?,?,?,?,?,?,?,?,?,?);";
|
||||
|
||||
update(start, INSERT, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, options.generatedOptionsId);
|
||||
pst.setString(4, options.challenge);
|
||||
pst.setString(5, options.userEmail);
|
||||
pst.setString(6, options.relyingPartyId);
|
||||
pst.setString(7, options.origin);
|
||||
pst.setLong(8, options.expiresAt);
|
||||
pst.setLong(9, options.createdAt);
|
||||
pst.setString(10, options.relyingPartyName);
|
||||
pst.setString(11, options.userVerification);
|
||||
pst.setBoolean(12, options.userPresenceRequired);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public static WebAuthNOptions loadOptionsById(Start start, TenantIdentifier tenantIdentifier, String optionsId)
|
||||
throws SQLException, StorageQueryException {
|
||||
String QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable()
|
||||
+ " WHERE app_id = ? AND tenant_id = ? and id = ?";
|
||||
return execute(start, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, optionsId);
|
||||
}, result -> {
|
||||
if(result.next()){
|
||||
return WebAuthNOptionsRowMapper.getInstance().mapOrThrow(result); // we are expecting one or zero results
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static WebAuthNStoredCredential loadCredentialByIdForUser(Start start, TenantIdentifier tenantIdentifier, String credentialId, String recipeUserId)
|
||||
throws StorageQueryException, SQLException {
|
||||
String QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNCredentialsTable()
|
||||
+ " WHERE app_id = ? AND id = ? AND user_id = ?";
|
||||
return execute(start, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, credentialId);
|
||||
pst.setString(3, recipeUserId);
|
||||
}, result -> {
|
||||
if(result.next()){
|
||||
return WebAuthnStoredCredentialRowMapper.getInstance().mapOrThrow(result); // we are expecting one or zero results
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static WebAuthNStoredCredential loadCredentialById_Transaction(Start start, Connection sqlConnection, TenantIdentifier tenantIdentifier, String credentialId)
|
||||
throws SQLException, StorageQueryException {
|
||||
String QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNCredentialsTable()
|
||||
+ " WHERE app_id = ? AND id = ?";
|
||||
return execute(sqlConnection, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, credentialId);
|
||||
}, result -> {
|
||||
if(result.next()){
|
||||
return WebAuthnStoredCredentialRowMapper.getInstance().mapOrThrow(result); // we are expecting one or zero results
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static WebAuthNStoredCredential saveCredential(Start start, TenantIdentifier tenantIdentifier, WebAuthNStoredCredential credential)
|
||||
throws SQLException, StorageQueryException {
|
||||
String INSERT = "INSERT INTO " + Config.getConfig(start).getWebAuthNCredentialsTable()
|
||||
+ " (id, app_id, rp_id, user_id, counter, public_key, transports, created_at, updated_at) "
|
||||
+ " VALUES (?,?,?,?,?,?,?,?,?);";
|
||||
|
||||
update(start, INSERT, pst -> {
|
||||
pst.setString(1, credential.id);
|
||||
pst.setString(2, credential.appId);
|
||||
pst.setString(3, credential.rpId);
|
||||
pst.setString(4, credential.userId);
|
||||
pst.setLong(5, credential.counter);
|
||||
pst.setBytes(6, credential.publicKey);
|
||||
pst.setString(7, credential.transports);
|
||||
pst.setLong(8, credential.createdAt);
|
||||
pst.setLong(9, credential.updatedAt);
|
||||
});
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
public static WebAuthNStoredCredential saveCredential_Transaction(Start start, Connection connection, TenantIdentifier tenantIdentifier, WebAuthNStoredCredential credential)
|
||||
throws SQLException, StorageQueryException {
|
||||
String INSERT = "INSERT INTO " + Config.getConfig(start).getWebAuthNCredentialsTable()
|
||||
+ " (id, app_id, rp_id, user_id, counter, public_key, transports, created_at, updated_at) "
|
||||
+ " VALUES (?,?,?,?,?,?,?,?,?);";
|
||||
|
||||
update(connection, INSERT, pst -> {
|
||||
pst.setString(1, credential.id);
|
||||
pst.setString(2, credential.appId);
|
||||
pst.setString(3, credential.rpId);
|
||||
pst.setString(4, credential.userId);
|
||||
pst.setLong(5, credential.counter);
|
||||
pst.setBytes(6, credential.publicKey);
|
||||
pst.setString(7, credential.transports);
|
||||
pst.setLong(8, credential.createdAt);
|
||||
pst.setLong(9, credential.updatedAt);
|
||||
});
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
public static void createUser_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId, String email,
|
||||
String relyingPartyId)
|
||||
throws StorageTransactionLogicException, StorageQueryException {
|
||||
long timeJoined = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// app_id_to_user_id
|
||||
String insertAppIdToUserId = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable()
|
||||
+ "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)";
|
||||
update(sqlCon, insertAppIdToUserId, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, userId);
|
||||
pst.setString(3, userId);
|
||||
pst.setString(4, WEBAUTHN.toString());
|
||||
});
|
||||
|
||||
// all_auth_recipe_users
|
||||
String insertAllAuthRecipeUsers = "INSERT INTO " + getConfig(start).getUsersTable()
|
||||
+
|
||||
"(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, " +
|
||||
"primary_or_recipe_user_time_joined)" +
|
||||
" VALUES(?, ?, ?, ?, ?, ?, ?)";
|
||||
update(sqlCon, insertAllAuthRecipeUsers, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, userId);
|
||||
pst.setString(4, userId);
|
||||
pst.setString(5, WEBAUTHN.toString());
|
||||
pst.setLong(6, timeJoined);
|
||||
pst.setLong(7, timeJoined);
|
||||
});
|
||||
|
||||
// webauthn_user_to_tenant
|
||||
String insertWebauthNUsersToTenant =
|
||||
"INSERT INTO " + Config.getConfig(start).getWebAuthNUserToTenantTable()
|
||||
+ " (app_id, tenant_id, user_id, email) "
|
||||
+ " VALUES (?,?,?,?);";
|
||||
|
||||
update(sqlCon, insertWebauthNUsersToTenant, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, userId);
|
||||
pst.setString(4, email);
|
||||
});
|
||||
|
||||
// webauthn_users
|
||||
String insertWebauthNUsers = "INSERT INTO " + Config.getConfig(start).getWebAuthNUsersTable()
|
||||
+ " (app_id, user_id, email, rp_id, time_joined) "
|
||||
+ " VALUES (?,?,?,?,?);";
|
||||
|
||||
update(sqlCon, insertWebauthNUsers, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, userId);
|
||||
pst.setString(3, email);
|
||||
pst.setString(4, relyingPartyId);
|
||||
pst.setLong(5, timeJoined);
|
||||
});
|
||||
|
||||
} catch (SQLException throwables) {
|
||||
throw new StorageTransactionLogicException(throwables);
|
||||
}
|
||||
}
|
||||
|
||||
public static AuthRecipeUserInfo signUp_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId, String email,
|
||||
String relyingPartyId)
|
||||
throws StorageTransactionLogicException, StorageQueryException, SQLException {
|
||||
|
||||
createUser_Transaction(start, sqlCon, tenantIdentifier, userId, email, relyingPartyId);
|
||||
|
||||
return getAuthRecipeUserInfo(start, sqlCon,
|
||||
tenantIdentifier, userId);
|
||||
}
|
||||
|
||||
public static AuthRecipeUserInfo signUpWithCredentialRegister_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId, String email,
|
||||
String relyingPartyId, WebAuthNStoredCredential credential)
|
||||
throws StorageQueryException, StorageTransactionLogicException, SQLException {
|
||||
|
||||
createUser_Transaction(start, sqlCon, tenantIdentifier, userId, email, relyingPartyId);
|
||||
saveCredential_Transaction(start, sqlCon, tenantIdentifier, credential);
|
||||
|
||||
return getAuthRecipeUserInfo(start, sqlCon,
|
||||
tenantIdentifier, userId);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static AuthRecipeUserInfo getAuthRecipeUserInfo(Start start, Connection sqlCon,
|
||||
TenantIdentifier tenantIdentifier, String userId)
|
||||
throws SQLException, StorageQueryException {
|
||||
Collection<? extends LoginMethod> loginMethods = getUsersInfoUsingIdList_Transaction(start, sqlCon,
|
||||
Collections.singleton(userId), tenantIdentifier.toAppIdentifier());
|
||||
AuthRecipeUserInfo userInfo = null;
|
||||
if (!loginMethods.isEmpty()) {
|
||||
for (LoginMethod loginMethod : loginMethods) {
|
||||
if(userInfo == null) {
|
||||
userInfo = AuthRecipeUserInfo.create(userId, false, loginMethod);
|
||||
} else {
|
||||
userInfo.addLoginMethod(loginMethod);
|
||||
if(!loginMethod.getSupertokensUserId().equals(loginMethod.getSupertokensOrExternalUserId())){
|
||||
userInfo.setExternalUserId(loginMethod.getSupertokensOrExternalUserId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
public static String getPrimaryUserIdForTenantUsingEmail(Start start, TenantIdentifier tenantIdentifier,
|
||||
String email)
|
||||
throws StorageQueryException {
|
||||
try {
|
||||
return start.startTransaction(con -> {
|
||||
try {
|
||||
Connection sqlConnection = (Connection) con.getConnection();
|
||||
return getPrimaryUserIdForTenantUsingEmail_Transaction(start, sqlConnection, tenantIdentifier,
|
||||
email);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
});
|
||||
} catch (StorageTransactionLogicException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getPrimaryUserIdForTenantUsingEmail_Transaction(Start start, Connection sqlConnection,
|
||||
TenantIdentifier tenantIdentifier,
|
||||
String email)
|
||||
throws SQLException, StorageQueryException {
|
||||
String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
|
||||
+ "FROM " + getConfig(start).getWebAuthNUserToTenantTable() + " AS ep" +
|
||||
" JOIN " + getConfig(start).getUsersTable() + " AS all_users" +
|
||||
" ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" +
|
||||
" WHERE ep.app_id = ? AND ep.email = ? AND ep.tenant_id = ?";
|
||||
|
||||
return execute(sqlConnection, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, email);
|
||||
pst.setString(3, tenantIdentifier.getTenantId());
|
||||
}, result -> {
|
||||
if (result.next()) {
|
||||
return result.getString("user_id");
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static String getPrimaryUserIdForAppUsingEmail_Transaction(Start start, Connection sqlConnection,
|
||||
AppIdentifier appIdentifier, String email)
|
||||
throws SQLException, StorageQueryException {
|
||||
String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id "
|
||||
+ "FROM " + getConfig(start).getWebAuthNUserToTenantTable() + " AS ep" +
|
||||
" JOIN " + getConfig(start).getUsersTable() + " AS all_users" +
|
||||
" ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" +
|
||||
" WHERE ep.app_id = ? AND ep.email = ?";
|
||||
|
||||
return execute(sqlConnection, QUERY, pst -> {
|
||||
pst.setString(1, appIdentifier.getAppId());
|
||||
pst.setString(2, email);
|
||||
}, result -> {
|
||||
if (result.next()) {
|
||||
return result.getString("user_id");
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static Collection<? extends LoginMethod> getUsersInfoUsingIdList(Start start, Set<String> ids, AppIdentifier appIdentifier)
|
||||
throws StorageQueryException {
|
||||
try {
|
||||
return start.startTransaction(con -> {
|
||||
Connection sqlConnection = (Connection) con.getConnection();
|
||||
try {
|
||||
return getUsersInfoUsingIdList_Transaction(start, sqlConnection, ids, appIdentifier);
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
});
|
||||
} catch (StorageTransactionLogicException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Collection<? extends LoginMethod> getUsersInfoUsingIdList_Transaction(Start start, Connection connection, Set<String> ids, AppIdentifier appIdentifier)
|
||||
throws SQLException, StorageQueryException {
|
||||
if (!ids.isEmpty()) {
|
||||
|
||||
String webauthnUsersTable = getConfig(start).getWebAuthNUsersTable();
|
||||
String credentialTable = getConfig(start).getWebAuthNCredentialsTable();
|
||||
String usersTable = getConfig(start).getUsersTable();
|
||||
String userIdMappingTable = getConfig(start).getUserIdMappingTable();
|
||||
String emailVerificationTable = getConfig(start).getEmailVerificationTable();
|
||||
|
||||
String queryAll = "SELECT webauthn.user_id as user_id, webauthn.email as email, webauthn.time_joined as time_joined, " +
|
||||
"credentials.id as credential_id, email_verification.email as email_verified, user_id_mapping.external_user_id as external_user_id," +
|
||||
"all_users.tenant_id as tenant_id " +
|
||||
"FROM " + webauthnUsersTable + " as webauthn " +
|
||||
"JOIN " + usersTable + " as all_users ON webauthn.app_id = all_users.app_id AND webauthn.user_id = all_users.user_id " +
|
||||
"LEFT JOIN " + credentialTable + " as credentials ON webauthn.user_id = credentials.user_id " +
|
||||
"LEFT JOIN " + userIdMappingTable + " as user_id_mapping ON webauthn.user_id = user_id_mapping.supertokens_user_id " +
|
||||
"LEFT JOIN " + emailVerificationTable + " as email_verification ON webauthn.app_id = email_verification.app_id AND (user_id_mapping.external_user_id = email_verification.user_id OR user_id_mapping.supertokens_user_id = email_verification.user_id OR webauthn.user_id = email_verification.user_id) " +
|
||||
" AND email_verification.email = webauthn.email " +
|
||||
"WHERE webauthn.app_id = ? AND webauthn.user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ")";
|
||||
|
||||
return execute(connection, queryAll, pst -> {
|
||||
pst.setString(1, appIdentifier.getAppId());
|
||||
int index = 2;
|
||||
for (String id : ids) {
|
||||
pst.setString(index++, id);
|
||||
}
|
||||
}, result -> {
|
||||
Map<String, LoginMethod> users = new HashMap<>();
|
||||
while (result.next()) {
|
||||
String userId = result.getString("user_id");
|
||||
String email = result.getString("email");
|
||||
long timeJoined = result.getLong("time_joined");
|
||||
String credentialId = result.getString("credential_id");
|
||||
boolean emailVerified = result.getString("email_verified") != null;
|
||||
String externalUserId = result.getString("external_user_id");
|
||||
String tenantId = result.getString("tenant_id");
|
||||
if(users.containsKey(userId)) {
|
||||
users.get(userId).webauthN.addCredentialId(credentialId);
|
||||
users.get(userId).tenantIds.add(tenantId);
|
||||
} else {
|
||||
List<String> credentialIds = new ArrayList<>();
|
||||
credentialIds.add(credentialId);
|
||||
LoginMethod loginMethod = new LoginMethod(userId, timeJoined, emailVerified, email, new LoginMethod.WebAuthN(credentialIds), new String[]{tenantId});
|
||||
loginMethod.setExternalUserId(externalUserId);
|
||||
users.put(userId, loginMethod);
|
||||
}
|
||||
}
|
||||
return users.values();
|
||||
});
|
||||
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public static AuthRecipeUserInfo getUserInfoByCredentialId_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String credentialId)
|
||||
throws SQLException, StorageQueryException {
|
||||
|
||||
String QUERY = "SELECT webauthn.user_id as user_id, webauthn.email as email, webauthn.time_joined as time_joined, " +
|
||||
"credentials.id as credential_id, email_verification.email as email_verified, user_id_mapping.external_user_id as external_user_id," +
|
||||
"all_users.tenant_id as tenant_id " +
|
||||
"FROM " + getConfig(start).getWebAuthNUsersTable() + " as webauthn " +
|
||||
"JOIN " + getConfig(start).getUsersTable() + " as all_users ON webauthn.app_id = all_users.app_id AND webauthn.user_id = all_users.user_id " +
|
||||
"LEFT JOIN " + getConfig(start).getWebAuthNCredentialsTable() + " as credentials ON webauthn.user_id = credentials.user_id " +
|
||||
"LEFT JOIN " + getConfig(start).getUserIdMappingTable() + " as user_id_mapping ON webauthn.user_id = user_id_mapping.supertokens_user_id " +
|
||||
"LEFT JOIN " + getConfig(start).getEmailVerificationTable() + " as email_verification ON webauthn.app_id = email_verification.app_id AND (user_id_mapping.external_user_id = email_verification.user_id OR user_id_mapping.supertokens_user_id = email_verification.user_id OR webauthn.user_id = email_verification.user_id)" +
|
||||
" AND email_verification.email = webauthn.email " +
|
||||
"WHERE webauthn.app_id = ? AND credentials.id = ?";
|
||||
|
||||
return execute(sqlCon, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, credentialId);
|
||||
}, result -> {
|
||||
if (result.next()) {
|
||||
String userId = result.getString("user_id");
|
||||
String email = result.getString("email");
|
||||
long timeJoined = result.getLong("time_joined");
|
||||
boolean emailVerified = result.getString("email_verified") != null;
|
||||
String externalUserId = result.getString("external_user_id");
|
||||
String tenantId = result.getString("tenant_id");
|
||||
List<String> credentialIds = new ArrayList<>();
|
||||
credentialIds.add(credentialId);
|
||||
LoginMethod.WebAuthN webAuthNLM = new LoginMethod.WebAuthN(credentialIds);
|
||||
LoginMethod loginMethod = new LoginMethod(userId, timeJoined, emailVerified, email, webAuthNLM, new String[]{tenantId});
|
||||
if(externalUserId != null) {
|
||||
loginMethod.setExternalUserId(externalUserId);
|
||||
}
|
||||
AuthRecipeUserInfo resultUserInfo = AuthRecipeUserInfo.create(userId, false, loginMethod);
|
||||
resultUserInfo.setExternalUserId(externalUserId);
|
||||
return resultUserInfo;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static WebAuthNOptions loadOptionsById_Transaction(Start start, Connection sqlCon,
|
||||
TenantIdentifier tenantIdentifier, String optionsId)
|
||||
throws SQLException, StorageQueryException {
|
||||
String QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable()
|
||||
+ " WHERE app_id = ? AND id = ?";
|
||||
return execute(sqlCon, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, optionsId);
|
||||
}, result -> {
|
||||
if(result.next()){
|
||||
return WebAuthNOptionsRowMapper.getInstance().mapOrThrow(result); // we are expecting one or zero results
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static void updateCounter_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String credentialId, long counter)
|
||||
throws SQLException, StorageQueryException {
|
||||
String UPDATE = "UPDATE " + Config.getConfig(start).getWebAuthNCredentialsTable()
|
||||
+ " SET counter = ?, updated_at = ? WHERE app_id = ? AND id = ?";
|
||||
|
||||
update(sqlCon, UPDATE, pst -> {
|
||||
pst.setLong(1, counter);
|
||||
pst.setLong(2, System.currentTimeMillis());
|
||||
pst.setString(3, tenantIdentifier.getAppId());
|
||||
pst.setString(4, credentialId);
|
||||
});
|
||||
}
|
||||
|
||||
public static int removeCredential(Start start, TenantIdentifier tenantIdentifier, String userId, String credentialId)
|
||||
throws SQLException, StorageQueryException {
|
||||
String UPDATE = "DELETE FROM " + Config.getConfig(start).getWebAuthNCredentialsTable()
|
||||
+ " WHERE app_id = ? AND id = ? AND user_id = ?";
|
||||
|
||||
return update(start, UPDATE, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, credentialId);
|
||||
pst.setString(3, userId);
|
||||
});
|
||||
}
|
||||
|
||||
public static int removeOptions(Start start, TenantIdentifier tenantIdentifier, String optionsId)
|
||||
throws SQLException, StorageQueryException {
|
||||
String UPDATE = "DELETE FROM " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable()
|
||||
+ " WHERE app_id = ? AND tenant_id = ? AND id = ?";
|
||||
|
||||
return update(start, UPDATE, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, optionsId);
|
||||
});
|
||||
}
|
||||
|
||||
public static List<WebAuthNStoredCredential> listCredentials(Start start, TenantIdentifier tenantIdentifier,
|
||||
String recipeUserId) throws SQLException, StorageQueryException {
|
||||
String LIST_QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNCredentialsTable() +
|
||||
" WHERE app_id = ? AND user_id = ?";
|
||||
return execute(start, LIST_QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, recipeUserId);
|
||||
}, result -> {
|
||||
List<WebAuthNStoredCredential> credentials = new ArrayList<>();
|
||||
while (result.next()) {
|
||||
credentials.add(WebAuthnStoredCredentialRowMapper.getInstance().mapOrThrow(result));
|
||||
}
|
||||
return credentials;
|
||||
});
|
||||
}
|
||||
|
||||
public static void updateUserEmail(Start start, TenantIdentifier tenantIdentifier, String userId, String newEmail)
|
||||
throws StorageQueryException {
|
||||
try {
|
||||
start.startTransaction(con -> {
|
||||
updateUserEmail_Transaction(start, (Connection) con.getConnection(), tenantIdentifier, userId, newEmail);
|
||||
return null;
|
||||
});
|
||||
} catch (StorageTransactionLogicException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateUserEmail_Transaction(Start start, Connection sqlConnection, TenantIdentifier tenantIdentifier,
|
||||
String userId, String newEmail) throws StorageQueryException {
|
||||
try {
|
||||
String UPDATE_USER_TO_TENANT_QUERY =
|
||||
"UPDATE " + Config.getConfig(start).getWebAuthNUserToTenantTable() +
|
||||
" SET email = ? WHERE app_id = ? AND tenant_id = ? AND user_id = ?";
|
||||
String UPDATE_USER_QUERY = "UPDATE " + Config.getConfig(start).getWebAuthNUsersTable() +
|
||||
" SET email = ? WHERE app_id = ? AND user_id = ?";
|
||||
|
||||
update(sqlConnection, UPDATE_USER_TO_TENANT_QUERY, pst -> {
|
||||
pst.setString(1, newEmail);
|
||||
pst.setString(2, tenantIdentifier.getAppId());
|
||||
pst.setString(3, tenantIdentifier.getTenantId());
|
||||
pst.setString(4, userId);
|
||||
});
|
||||
|
||||
update(sqlConnection, UPDATE_USER_QUERY, pst -> {
|
||||
pst.setString(1, newEmail);
|
||||
pst.setString(2, tenantIdentifier.getAppId());
|
||||
pst.setString(3, userId);
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
throw new StorageQueryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class WebAuthnStoredCredentialRowMapper implements RowMapper<WebAuthNStoredCredential, ResultSet> {
|
||||
private static final WebAuthnStoredCredentialRowMapper INSTANCE = new WebAuthnStoredCredentialRowMapper();
|
||||
|
||||
public static WebAuthnStoredCredentialRowMapper getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthNStoredCredential map(ResultSet rs) throws Exception {
|
||||
WebAuthNStoredCredential result = new WebAuthNStoredCredential();
|
||||
result.id = rs.getString("id");
|
||||
result.appId = rs.getString("app_id");
|
||||
result.rpId = rs.getString("rp_id");
|
||||
result.userId = rs.getString("user_id");
|
||||
result.counter = rs.getLong("counter");
|
||||
result.publicKey = rs.getBytes("public_key");
|
||||
result.transports = rs.getString("transports");
|
||||
result.createdAt = rs.getLong("created_at");
|
||||
result.updatedAt = rs.getLong("updated_at");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static class WebAuthNOptionsRowMapper implements RowMapper<WebAuthNOptions, ResultSet> {
|
||||
private static final WebAuthNOptionsRowMapper INSTANCE = new WebAuthNOptionsRowMapper();
|
||||
|
||||
public static WebAuthNOptionsRowMapper getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthNOptions map(ResultSet rs) throws Exception {
|
||||
WebAuthNOptions result = new WebAuthNOptions();
|
||||
result.timeout = rs.getLong("expires_at") - rs.getLong("created_at");
|
||||
result.expiresAt = rs.getLong("expires_at");
|
||||
result.createdAt = rs.getLong("created_at");
|
||||
result.relyingPartyId = rs.getString("rp_id");
|
||||
result.origin = rs.getString("origin");
|
||||
result.challenge = rs.getString("challenge");
|
||||
result.userEmail = rs.getString("email");
|
||||
result.generatedOptionsId = rs.getString("id");
|
||||
result.relyingPartyName = rs.getString("rp_name");
|
||||
result.userPresenceRequired = rs.getBoolean("user_presence_required");
|
||||
result.userVerification = rs.getString("user_verification");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static void addRecoverAccountToken(Start start, TenantIdentifier tenantIdentifier, AccountRecoveryTokenInfo accountRecoveryTokenInfo)
|
||||
throws SQLException, StorageQueryException {
|
||||
String INSERT = "INSERT INTO " + Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() + " (app_id, tenant_id, user_id, email, token, expires_at) VALUES (?, ?, ?, ?, ?, ?)";
|
||||
update(start, INSERT, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, accountRecoveryTokenInfo.userId);
|
||||
pst.setString(4, accountRecoveryTokenInfo.email);
|
||||
pst.setString(5, accountRecoveryTokenInfo.token);
|
||||
pst.setLong(6, accountRecoveryTokenInfo.expiresAt);
|
||||
});
|
||||
}
|
||||
|
||||
public static AccountRecoveryTokenInfo getAccountRecoveryTokenInfoByToken_Transaction(Start start, TenantIdentifier tenantIdentifier, Connection con, String token)
|
||||
throws SQLException, StorageQueryException {
|
||||
String QUERY = "SELECT * FROM " + Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() + " WHERE app_id = ? AND tenant_id = ? AND token = ?";
|
||||
return execute(con, QUERY, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, token);
|
||||
}, result -> {
|
||||
if (result.next()) {
|
||||
return AccountRecoveryTokenInfoRowMapper.getInstance().mapOrThrow(result);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private static class AccountRecoveryTokenInfoRowMapper implements RowMapper<AccountRecoveryTokenInfo, ResultSet> {
|
||||
private static final AccountRecoveryTokenInfoRowMapper INSTANCE = new AccountRecoveryTokenInfoRowMapper();
|
||||
|
||||
public static AccountRecoveryTokenInfoRowMapper getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountRecoveryTokenInfo map(ResultSet rs) throws Exception {
|
||||
AccountRecoveryTokenInfo result = new AccountRecoveryTokenInfo(
|
||||
rs.getString("user_id"),
|
||||
rs.getString("email"),
|
||||
rs.getString("token"),
|
||||
rs.getLong("expires_at")
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteAccountRecoveryTokenByEmail_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String email)
|
||||
throws SQLException, StorageQueryException {
|
||||
String DELETE = "DELETE FROM " + Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() + " WHERE app_id = ? AND tenant_id = ? AND email = ?";
|
||||
update(con, DELETE, pst -> {
|
||||
pst.setString(1, tenantIdentifier.getAppId());
|
||||
pst.setString(2, tenantIdentifier.getTenantId());
|
||||
pst.setString(3, email);
|
||||
});
|
||||
}
|
||||
|
||||
public static void deleteExpiredAccountRecoveryTokens(Start start)
|
||||
throws SQLException, StorageQueryException {
|
||||
String DELETE = "DELETE FROM " + Config.getConfig(start).getWebAuthNAccountRecoveryTokenTable() + " WHERE expires_at < ?";
|
||||
update(start, DELETE, pst -> {
|
||||
pst.setLong(1, System.currentTimeMillis());
|
||||
});
|
||||
}
|
||||
|
||||
public static void deleteExpiredGeneratedOptions(Start start)
|
||||
throws SQLException, StorageQueryException {
|
||||
String DELETE = "DELETE FROM " + Config.getConfig(start).getWebAuthNGeneratedOptionsTable() + " WHERE expires_at < ?";
|
||||
update(start, DELETE, pst -> {
|
||||
pst.setLong(1, System.currentTimeMillis());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import com.google.gson.Gson;
|
|||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import io.supertokens.Main;
|
||||
import io.supertokens.ResourceDistributor;
|
||||
import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
|
||||
|
|
@ -54,7 +55,7 @@ public class JWTSigningFunctions {
|
|||
throws StorageQueryException, StorageTransactionLogicException, NoSuchAlgorithmException,
|
||||
InvalidKeySpecException, JWTCreationException, UnsupportedJWTSigningAlgorithmException {
|
||||
try {
|
||||
return createJWTToken(new AppIdentifier(null, null), main, algorithm, payload, jwksDomain,
|
||||
return createJWTToken(ResourceDistributor.getAppForTesting().toAppIdentifier(), main, algorithm, payload, jwksDomain,
|
||||
jwtValidityInSeconds, useDynamicKey);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import io.supertokens.pluginInterface.exceptions.InvalidConfigException;
|
|||
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
|
||||
import io.supertokens.pluginInterface.multitenancy.*;
|
||||
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
|
||||
import io.supertokens.saml.SAMLCertificate;
|
||||
import io.supertokens.pluginInterface.opentelemetry.WithinOtelSpan;
|
||||
import io.supertokens.session.refreshToken.RefreshTokenKey;
|
||||
import io.supertokens.signingkeys.AccessTokenSigningKey;
|
||||
import io.supertokens.signingkeys.JWTSigningKey;
|
||||
|
|
@ -116,6 +118,7 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
|
|||
return StorageLayer.getMultitenancyStorage(main).getAllTenants();
|
||||
}
|
||||
|
||||
@WithinOtelSpan
|
||||
public List<TenantIdentifier> refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(
|
||||
boolean reloadAllResources) {
|
||||
try {
|
||||
|
|
@ -233,6 +236,7 @@ public class MultitenancyHelper extends ResourceDistributor.SingletonResource {
|
|||
}
|
||||
AccessTokenSigningKey.loadForAllTenants(main, apps, tenantsThatChanged);
|
||||
RefreshTokenKey.loadForAllTenants(main, apps, tenantsThatChanged);
|
||||
SAMLCertificate.loadForAllTenants(main, apps, tenantsThatChanged);
|
||||
JWTSigningKey.loadForAllTenants(main, apps, tenantsThatChanged);
|
||||
SigningKeys.loadForAllTenants(main, apps, tenantsThatChanged);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ public class HttpRequestForOAuthProvider {
|
|||
// case of errors, etc.
|
||||
// Left the original HttpRequest as is to avoid any issues with existing code.
|
||||
|
||||
private static final int CONNECTION_TIMEOUT = 5000;
|
||||
private static final int READ_TIMEOUT = 5000;
|
||||
private static final int CONNECTION_TIMEOUT = 15000;
|
||||
private static final int READ_TIMEOUT = 15000;
|
||||
|
||||
public static Response doGet(String url, Map<String, String> headers, Map<String, String> queryParams) throws IOException {
|
||||
if (queryParams == null) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.supertokens.output;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
|
|
@ -32,6 +33,7 @@ import io.supertokens.pluginInterface.Storage;
|
|||
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
||||
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
|
||||
import io.supertokens.storageLayer.StorageLayer;
|
||||
import io.supertokens.telemetry.TelemetryProvider;
|
||||
import io.supertokens.utils.Utils;
|
||||
import io.supertokens.version.Version;
|
||||
import io.supertokens.webserver.Webserver;
|
||||
|
|
@ -54,6 +56,12 @@ public class Logging extends ResourceDistributor.SingletonResource {
|
|||
public static final String ANSI_WHITE = "\u001B[37m";
|
||||
|
||||
private Logging(Main main) {
|
||||
// Set global logging level
|
||||
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
|
||||
Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||
Level newLevel = Level.toLevel(Config.getBaseConfig(main).getLogLevel(), Level.INFO); // Default to INFO if invalid
|
||||
rootLogger.setLevel(newLevel);
|
||||
|
||||
this.infoLogger = Config.getBaseConfig(main).getInfoLogPath(main).equals("null")
|
||||
? createLoggerForConsole(main, "io.supertokens.Info", LOG_LEVEL.INFO)
|
||||
: createLoggerForFile(main, Config.getBaseConfig(main).getInfoLogPath(main),
|
||||
|
|
@ -65,7 +73,7 @@ public class Logging extends ResourceDistributor.SingletonResource {
|
|||
Storage storage = StorageLayer.getBaseStorage(main);
|
||||
if (storage != null) {
|
||||
storage.initFileLogging(Config.getBaseConfig(main).getInfoLogPath(main),
|
||||
Config.getBaseConfig(main).getErrorLogPath(main));
|
||||
Config.getBaseConfig(main).getErrorLogPath(main), TelemetryProvider.getInstance(main));
|
||||
}
|
||||
try {
|
||||
// we wait here for a bit so that the loggers can be properly initialised..
|
||||
|
|
@ -111,7 +119,9 @@ public class Logging extends ResourceDistributor.SingletonResource {
|
|||
try {
|
||||
msg = msg.trim();
|
||||
if (getInstance(main) != null) {
|
||||
getInstance(main).infoLogger.debug(getFormattedMessage(tenantIdentifier, msg));
|
||||
String formattedMsg = getFormattedMessage(tenantIdentifier, msg);
|
||||
getInstance(main).infoLogger.debug(formattedMsg);
|
||||
TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, formattedMsg, "debug");
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
// sometimes logger.debug throws a null pointer exception...
|
||||
|
|
@ -162,6 +172,8 @@ public class Logging extends ResourceDistributor.SingletonResource {
|
|||
if (getInstance(main) != null) {
|
||||
getInstance(main).infoLogger.info(msg);
|
||||
}
|
||||
|
||||
TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, msg, "info");
|
||||
} catch (NullPointerException ignored) {
|
||||
}
|
||||
}
|
||||
|
|
@ -175,6 +187,7 @@ public class Logging extends ResourceDistributor.SingletonResource {
|
|||
msg = getFormattedMessage(tenantIdentifier, msg);
|
||||
if (getInstance(main) != null) {
|
||||
getInstance(main).errorLogger.warn(msg);
|
||||
TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, msg, "warn");
|
||||
}
|
||||
} catch (NullPointerException ignored) {
|
||||
}
|
||||
|
|
@ -194,7 +207,9 @@ public class Logging extends ResourceDistributor.SingletonResource {
|
|||
try {
|
||||
err = err.trim();
|
||||
if (getInstance(main) != null) {
|
||||
getInstance(main).errorLogger.error(getFormattedMessage(tenantIdentifier, err));
|
||||
String formattedMessage = getFormattedMessage(tenantIdentifier, err);
|
||||
getInstance(main).errorLogger.error(formattedMessage);
|
||||
TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, formattedMessage, "error");
|
||||
}
|
||||
if (toConsoleAsWell || getInstance(main) == null) {
|
||||
systemErr(prependTenantIdentifierToMessage(tenantIdentifier, err));
|
||||
|
|
@ -228,6 +243,9 @@ public class Logging extends ResourceDistributor.SingletonResource {
|
|||
message = message.trim();
|
||||
if (getInstance(main) != null) {
|
||||
getInstance(main).errorLogger.error(getFormattedMessage(tenantIdentifier, message, e));
|
||||
TelemetryProvider.getInstance(main)
|
||||
.createLogEvent(tenantIdentifier, getFormattedMessage(tenantIdentifier, message, e),
|
||||
"error");
|
||||
}
|
||||
if (toConsoleAsWell || getInstance(main) == null) {
|
||||
systemErr(prependTenantIdentifierToMessage(tenantIdentifier, message));
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.supertokens.passwordless;
|
||||
|
||||
import io.supertokens.Main;
|
||||
import io.supertokens.ResourceDistributor;
|
||||
import io.supertokens.authRecipe.AuthRecipe;
|
||||
import io.supertokens.config.Config;
|
||||
import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException;
|
||||
|
|
@ -74,7 +75,7 @@ public class Passwordless {
|
|||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return createCode(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
main, email, phoneNumber, deviceId, userInputCode);
|
||||
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -161,7 +162,7 @@ public class Passwordless {
|
|||
NoSuchAlgorithmException, Base64EncodingException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getDeviceWithCodesById(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
deviceId);
|
||||
}
|
||||
|
||||
|
|
@ -170,7 +171,7 @@ public class Passwordless {
|
|||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getDeviceWithCodesByIdHash(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
deviceIdHash);
|
||||
}
|
||||
|
||||
|
|
@ -210,7 +211,7 @@ public class Passwordless {
|
|||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getDevicesWithCodesByEmail(
|
||||
new TenantIdentifier(null, null, null), storage, email);
|
||||
ResourceDistributor.getAppForTesting(), storage, email);
|
||||
}
|
||||
|
||||
public static List<DeviceWithCodes> getDevicesWithCodesByPhoneNumber(
|
||||
|
|
@ -235,7 +236,7 @@ public class Passwordless {
|
|||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getDevicesWithCodesByPhoneNumber(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
phoneNumber);
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +250,7 @@ public class Passwordless {
|
|||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return consumeCode(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, false);
|
||||
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -266,7 +267,7 @@ public class Passwordless {
|
|||
try {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return consumeCode(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, setEmailVerified);
|
||||
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -567,7 +568,7 @@ public class Passwordless {
|
|||
public static void removeCode(Main main, String codeId)
|
||||
throws StorageQueryException, StorageTransactionLogicException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
removeCode(new TenantIdentifier(null, null, null), storage,
|
||||
removeCode(ResourceDistributor.getAppForTesting(), storage,
|
||||
codeId);
|
||||
}
|
||||
|
||||
|
|
@ -622,7 +623,7 @@ public class Passwordless {
|
|||
throws StorageQueryException, StorageTransactionLogicException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
removeCodesByEmail(
|
||||
new TenantIdentifier(null, null, null), storage, email);
|
||||
ResourceDistributor.getAppForTesting(), storage, email);
|
||||
}
|
||||
|
||||
public static void removeCodesByEmail(TenantIdentifier tenantIdentifier, Storage storage, String email)
|
||||
|
|
@ -642,7 +643,7 @@ public class Passwordless {
|
|||
throws StorageQueryException, StorageTransactionLogicException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
removeCodesByPhoneNumber(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
phoneNumber);
|
||||
}
|
||||
|
||||
|
|
@ -664,7 +665,7 @@ public class Passwordless {
|
|||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getUserById(
|
||||
new AppIdentifier(null, null), storage, userId);
|
||||
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
|
|
@ -689,7 +690,7 @@ public class Passwordless {
|
|||
String phoneNumber) throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getUserByPhoneNumber(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
phoneNumber);
|
||||
}
|
||||
|
||||
|
|
@ -714,7 +715,7 @@ public class Passwordless {
|
|||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getUserByEmail(
|
||||
new TenantIdentifier(null, null, null), storage, email);
|
||||
ResourceDistributor.getAppForTesting(), storage, email);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
|
|
@ -740,7 +741,7 @@ public class Passwordless {
|
|||
DuplicatePhoneNumberException, UserWithoutContactInfoException, EmailChangeNotAllowedException,
|
||||
PhoneNumberChangeNotAllowedException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
updateUser(new AppIdentifier(null, null), storage,
|
||||
updateUser(ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
|
||||
userId, emailUpdate, phoneNumberUpdate);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,690 @@
|
|||
/*
|
||||
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0 (the
|
||||
* "License") as published by the Apache Software Foundation.
|
||||
*
|
||||
* You may not use this file except in compliance with the License. You may
|
||||
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package io.supertokens.saml;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
import org.opensaml.core.xml.XMLObjectBuilderFactory;
|
||||
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
|
||||
import org.opensaml.core.xml.io.UnmarshallingException;
|
||||
import org.opensaml.core.xml.util.XMLObjectSupport;
|
||||
import org.opensaml.saml.common.SAMLVersion;
|
||||
import org.opensaml.saml.common.xml.SAMLConstants;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.opensaml.saml.saml2.core.Attribute;
|
||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||
import org.opensaml.saml.saml2.core.Audience;
|
||||
import org.opensaml.saml.saml2.core.AudienceRestriction;
|
||||
import org.opensaml.saml.saml2.core.AuthnContext;
|
||||
import org.opensaml.saml.saml2.core.AuthnContextClassRef;
|
||||
import org.opensaml.saml.saml2.core.AuthnRequest;
|
||||
import org.opensaml.saml.saml2.core.Conditions;
|
||||
import org.opensaml.saml.saml2.core.Issuer;
|
||||
import org.opensaml.saml.saml2.core.NameIDPolicy;
|
||||
import org.opensaml.saml.saml2.core.RequestedAuthnContext;
|
||||
import org.opensaml.saml.saml2.core.Response;
|
||||
import org.opensaml.saml.saml2.core.Subject;
|
||||
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
|
||||
import org.opensaml.security.credential.Credential;
|
||||
import org.opensaml.security.credential.CredentialSupport;
|
||||
import org.opensaml.xmlsec.signature.KeyInfo;
|
||||
import org.opensaml.xmlsec.signature.Signature;
|
||||
import org.opensaml.xmlsec.signature.X509Data;
|
||||
import org.opensaml.xmlsec.signature.impl.KeyInfoBuilder;
|
||||
import org.opensaml.xmlsec.signature.impl.SignatureBuilder;
|
||||
import org.opensaml.xmlsec.signature.impl.X509DataBuilder;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureException;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureValidator;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import io.supertokens.Main;
|
||||
import io.supertokens.config.Config;
|
||||
import io.supertokens.config.CoreConfig;
|
||||
import io.supertokens.featureflag.EE_FEATURES;
|
||||
import io.supertokens.featureflag.FeatureFlag;
|
||||
import io.supertokens.featureflag.exceptions.FeatureNotEnabledException;
|
||||
import io.supertokens.pluginInterface.Storage;
|
||||
import io.supertokens.pluginInterface.StorageUtils;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
|
||||
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
|
||||
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
||||
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
|
||||
import io.supertokens.pluginInterface.saml.SAMLClaimsInfo;
|
||||
import io.supertokens.pluginInterface.saml.SAMLClient;
|
||||
import io.supertokens.pluginInterface.saml.SAMLRelayStateInfo;
|
||||
import io.supertokens.pluginInterface.saml.SAMLStorage;
|
||||
import io.supertokens.pluginInterface.saml.exception.DuplicateEntityIdException;
|
||||
import io.supertokens.saml.exceptions.IDPInitiatedLoginDisallowedException;
|
||||
import io.supertokens.saml.exceptions.InvalidClientException;
|
||||
import io.supertokens.saml.exceptions.InvalidCodeException;
|
||||
import io.supertokens.saml.exceptions.InvalidRelayStateException;
|
||||
import io.supertokens.saml.exceptions.MalformedSAMLMetadataXMLException;
|
||||
import io.supertokens.saml.exceptions.SAMLResponseVerificationFailedException;
|
||||
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
|
||||
import net.shibboleth.utilities.java.support.xml.XMLParserException;
|
||||
|
||||
public class SAML {
|
||||
public static void checkForSAMLFeature(AppIdentifier appIdentifier, Main main)
|
||||
throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException {
|
||||
EE_FEATURES[] features = FeatureFlag.getInstance(main, appIdentifier).getEnabledFeatures();
|
||||
for (EE_FEATURES f : features) {
|
||||
if (f == EE_FEATURES.SAML) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new FeatureNotEnabledException(
|
||||
"SAML feature is not enabled. Please subscribe to a SuperTokens core license key to enable this " +
|
||||
"feature.");
|
||||
}
|
||||
|
||||
public static SAMLClient createOrUpdateSAMLClient(
|
||||
Main main, TenantIdentifier tenantIdentifier, Storage storage,
|
||||
String clientId, String clientSecret, String defaultRedirectURI, JsonArray redirectURIs, String metadataXML, boolean allowIDPInitiatedLogin, boolean enableRequestSigning)
|
||||
throws MalformedSAMLMetadataXMLException, StorageQueryException, CertificateException,
|
||||
FeatureNotEnabledException, TenantOrAppNotFoundException, DuplicateEntityIdException {
|
||||
checkForSAMLFeature(tenantIdentifier.toAppIdentifier(), main);
|
||||
|
||||
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
|
||||
|
||||
var metadata = loadIdpMetadata(metadataXML);
|
||||
String idpSsoUrl = null;
|
||||
for (var roleDescriptor : metadata.getRoleDescriptors()) {
|
||||
if (roleDescriptor instanceof IDPSSODescriptor) {
|
||||
IDPSSODescriptor idpDescriptor = (IDPSSODescriptor) roleDescriptor;
|
||||
for (SingleSignOnService ssoService : idpDescriptor.getSingleSignOnServices()) {
|
||||
if (SAMLConstants.SAML2_REDIRECT_BINDING_URI.equals(ssoService.getBinding())) {
|
||||
idpSsoUrl = ssoService.getLocation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (idpSsoUrl == null) {
|
||||
throw new MalformedSAMLMetadataXMLException();
|
||||
}
|
||||
|
||||
String idpSigningCertificate = extractIdpSigningCertificate(metadata);
|
||||
getCertificateFromString(idpSigningCertificate); // checking validity
|
||||
|
||||
String idpEntityId = metadata.getEntityID();
|
||||
SAMLClient client = new SAMLClient(clientId, clientSecret, idpSsoUrl, redirectURIs, defaultRedirectURI, idpEntityId, idpSigningCertificate, allowIDPInitiatedLogin, enableRequestSigning);
|
||||
return samlStorage.createOrUpdateSAMLClient(tenantIdentifier, client);
|
||||
}
|
||||
|
||||
public static List<SAMLClient> getClients(TenantIdentifier tenantIdentifier, Storage storage) throws StorageQueryException {
|
||||
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
|
||||
return samlStorage.getSAMLClients(tenantIdentifier);
|
||||
}
|
||||
|
||||
public static SAMLClient getClient(TenantIdentifier tenantIdentifier, Storage storage, String clientId) throws StorageQueryException {
|
||||
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
|
||||
return samlStorage.getSAMLClient(tenantIdentifier, clientId);
|
||||
}
|
||||
|
||||
public static boolean removeSAMLClient(TenantIdentifier tenantIdentifier, Storage storage, String clientId) throws StorageQueryException {
|
||||
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
|
||||
return samlStorage.removeSAMLClient(tenantIdentifier, clientId);
|
||||
}
|
||||
|
||||
private static String extractIdpSigningCertificate(EntityDescriptor idpMetadata) {
|
||||
for (var roleDescriptor : idpMetadata.getRoleDescriptors()) {
|
||||
if (roleDescriptor instanceof IDPSSODescriptor) {
|
||||
IDPSSODescriptor idpDescriptor = (IDPSSODescriptor) roleDescriptor;
|
||||
for (org.opensaml.saml.saml2.metadata.KeyDescriptor keyDescriptor : idpDescriptor.getKeyDescriptors()) {
|
||||
if (keyDescriptor.getUse() == null ||
|
||||
"SIGNING".equals(keyDescriptor.getUse().toString())) {
|
||||
org.opensaml.xmlsec.signature.KeyInfo keyInfo = keyDescriptor.getKeyInfo();
|
||||
if (keyInfo != null) {
|
||||
for (org.opensaml.xmlsec.signature.X509Data x509Data : keyInfo.getX509Datas()) {
|
||||
for (org.opensaml.xmlsec.signature.X509Certificate x509Cert : x509Data.getX509Certificates()) {
|
||||
try {
|
||||
String certString = x509Cert.getValue();
|
||||
if (certString != null && !certString.trim().isEmpty()) {
|
||||
certString = certString.replaceAll("\\s", "");
|
||||
return certString;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Continue to next certificate if this one fails
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
public static String createRedirectURL(Main main, TenantIdentifier tenantIdentifier, Storage storage,
|
||||
String clientId, String redirectURI, String state, String acsURL)
|
||||
throws StorageQueryException, InvalidClientException, TenantOrAppNotFoundException,
|
||||
CertificateEncodingException, FeatureNotEnabledException {
|
||||
checkForSAMLFeature(tenantIdentifier.toAppIdentifier(), main);
|
||||
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
|
||||
CoreConfig config = Config.getConfig(tenantIdentifier, main);
|
||||
|
||||
SAMLClient client = samlStorage.getSAMLClient(tenantIdentifier, clientId);
|
||||
|
||||
if (client == null) {
|
||||
throw new InvalidClientException();
|
||||
}
|
||||
|
||||
boolean redirectURIOk = false;
|
||||
for (JsonElement rUri : client.redirectURIs) {
|
||||
if (rUri.getAsString().equals(redirectURI)) {
|
||||
redirectURIOk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!redirectURIOk) {
|
||||
throw new InvalidClientException();
|
||||
}
|
||||
|
||||
String idpSsoUrl = client.ssoLoginURL;
|
||||
AuthnRequest request = buildAuthnRequest(
|
||||
main,
|
||||
tenantIdentifier.toAppIdentifier(),
|
||||
idpSsoUrl,
|
||||
config.getSAMLSPEntityID(), acsURL,
|
||||
client.enableRequestSigning);
|
||||
String samlRequest = deflateAndBase64RedirectMessage(request);
|
||||
String relayState = UUID.randomUUID().toString();
|
||||
|
||||
samlStorage.saveRelayStateInfo(tenantIdentifier, new SAMLRelayStateInfo(relayState, clientId, state, redirectURI), config.getSAMLRelayStateValidity());
|
||||
|
||||
return idpSsoUrl + "?SAMLRequest=" + samlRequest + "&RelayState=" + URLEncoder.encode(relayState, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static EntityDescriptor loadIdpMetadata(String metadataXML) throws MalformedSAMLMetadataXMLException {
|
||||
try {
|
||||
byte[] bytes = metadataXML.getBytes(StandardCharsets.UTF_8);
|
||||
try (InputStream inputStream = new java.io.ByteArrayInputStream(bytes)) {
|
||||
XMLObject xmlObject = XMLObjectSupport.unmarshallFromInputStream(
|
||||
XMLObjectProviderRegistrySupport.getParserPool(), inputStream);
|
||||
if (xmlObject instanceof EntityDescriptor) {
|
||||
return (EntityDescriptor) xmlObject;
|
||||
} else {
|
||||
throw new RuntimeException("Expected EntityDescriptor but got: " + xmlObject.getClass());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new MalformedSAMLMetadataXMLException();
|
||||
}
|
||||
}
|
||||
|
||||
private static AuthnRequest buildAuthnRequest(Main main, AppIdentifier appIdentifier, String idpSsoUrl, String spEntityId, String acsUrl, boolean enableRequestSigning)
|
||||
throws TenantOrAppNotFoundException, StorageQueryException, CertificateEncodingException {
|
||||
XMLObjectBuilderFactory builders = XMLObjectProviderRegistrySupport.getBuilderFactory();
|
||||
|
||||
AuthnRequest authnRequest = (AuthnRequest) builders
|
||||
.<AuthnRequest>getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME)
|
||||
.buildObject(AuthnRequest.DEFAULT_ELEMENT_NAME);
|
||||
authnRequest.setID("_" + UUID.randomUUID());
|
||||
authnRequest.setIssueInstant(Instant.now());
|
||||
authnRequest.setVersion(SAMLVersion.VERSION_20);
|
||||
authnRequest.setDestination(idpSsoUrl);
|
||||
authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
|
||||
|
||||
Issuer issuer = (Issuer) builders.getBuilder(Issuer.DEFAULT_ELEMENT_NAME)
|
||||
.buildObject(Issuer.DEFAULT_ELEMENT_NAME);
|
||||
issuer.setValue(spEntityId);
|
||||
authnRequest.setIssuer(issuer);
|
||||
|
||||
NameIDPolicy nameIDPolicy = (NameIDPolicy) builders.getBuilder(NameIDPolicy.DEFAULT_ELEMENT_NAME)
|
||||
.buildObject(NameIDPolicy.DEFAULT_ELEMENT_NAME);
|
||||
nameIDPolicy.setAllowCreate(true);
|
||||
authnRequest.setNameIDPolicy(nameIDPolicy);
|
||||
|
||||
RequestedAuthnContext rac = (RequestedAuthnContext) builders.getBuilder(RequestedAuthnContext.DEFAULT_ELEMENT_NAME)
|
||||
.buildObject(RequestedAuthnContext.DEFAULT_ELEMENT_NAME);
|
||||
rac.setComparison(org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration.EXACT);
|
||||
AuthnContextClassRef classRef = (AuthnContextClassRef) builders.getBuilder(AuthnContextClassRef.DEFAULT_ELEMENT_NAME)
|
||||
.buildObject(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
|
||||
classRef.setURI(AuthnContext.PASSWORD_AUTHN_CTX);
|
||||
rac.getAuthnContextClassRefs().add(classRef);
|
||||
authnRequest.setRequestedAuthnContext(rac);
|
||||
|
||||
authnRequest.setAssertionConsumerServiceURL(acsUrl);
|
||||
|
||||
if (enableRequestSigning) {
|
||||
Signature signature = new SignatureBuilder().buildObject();
|
||||
signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
|
||||
signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
|
||||
|
||||
// Create KeyInfo
|
||||
KeyInfo keyInfo = new KeyInfoBuilder().buildObject();
|
||||
X509Data x509Data = new X509DataBuilder().buildObject();
|
||||
org.opensaml.xmlsec.signature.X509Certificate x509CertElement = new org.opensaml.xmlsec.signature.impl.X509CertificateBuilder().buildObject();
|
||||
|
||||
X509Certificate spCertificate = SAMLCertificate.getInstance(appIdentifier, main).getCertificate();
|
||||
String certString = java.util.Base64.getEncoder().encodeToString(spCertificate.getEncoded());
|
||||
x509CertElement.setValue(certString);
|
||||
x509Data.getX509Certificates().add(x509CertElement);
|
||||
keyInfo.getX509Datas().add(x509Data);
|
||||
signature.setKeyInfo(keyInfo);
|
||||
|
||||
authnRequest.setSignature(signature);
|
||||
}
|
||||
|
||||
return authnRequest;
|
||||
}
|
||||
|
||||
private static String deflateAndBase64RedirectMessage(XMLObject xmlObject) {
|
||||
try {
|
||||
String xml = toXmlString(xmlObject);
|
||||
byte[] xmlBytes = xml.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// DEFLATE compression as per SAML Redirect binding spec
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
DeflaterOutputStream dos = new DeflaterOutputStream(baos, new Deflater(Deflater.DEFLATED, true));
|
||||
dos.write(xmlBytes);
|
||||
dos.close();
|
||||
|
||||
byte[] deflated = baos.toByteArray();
|
||||
String base64 = java.util.Base64.getEncoder().encodeToString(deflated);
|
||||
return URLEncoder.encode(base64, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to deflate SAML message", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String toXmlString(XMLObject xmlObject) {
|
||||
try {
|
||||
Element el = XMLObjectSupport.marshall(xmlObject);
|
||||
return SerializeSupport.nodeToString(el);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to serialize XML", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Response parseSamlResponse(String samlResponseBase64)
|
||||
throws IOException, XMLParserException, UnmarshallingException {
|
||||
byte[] decoded = java.util.Base64.getDecoder().decode(samlResponseBase64);
|
||||
String xml = new String(decoded, StandardCharsets.UTF_8);
|
||||
|
||||
try (InputStream inputStream = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) {
|
||||
return (Response) XMLObjectSupport.unmarshallFromInputStream(
|
||||
XMLObjectProviderRegistrySupport.getParserPool(), inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifySamlResponseSignature(Response samlResponse, X509Certificate idpCertificate)
|
||||
throws SignatureException {
|
||||
Signature responseSignature = samlResponse.getSignature();
|
||||
if (responseSignature != null) {
|
||||
Credential credential = CredentialSupport.getSimpleCredential(idpCertificate, null);
|
||||
SignatureValidator.validate(responseSignature, credential);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean foundSignedAssertion = false;
|
||||
for (Assertion assertion : samlResponse.getAssertions()) {
|
||||
Signature assertionSignature = assertion.getSignature();
|
||||
if (assertionSignature != null) {
|
||||
Credential credential = CredentialSupport.getSimpleCredential(idpCertificate, null);
|
||||
SignatureValidator.validate(assertionSignature, credential);
|
||||
foundSignedAssertion = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundSignedAssertion) {
|
||||
throw new RuntimeException("Neither SAML Response nor any Assertion is signed");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateSamlResponseTimestamps(Response samlResponse) throws SAMLResponseVerificationFailedException {
|
||||
Instant now = Instant.now();
|
||||
|
||||
// Validate response issue instant (should be recent)
|
||||
if (samlResponse.getIssueInstant() != null) {
|
||||
Instant responseTime = samlResponse.getIssueInstant();
|
||||
// Allow 5 minutes clock skew
|
||||
if (responseTime.isAfter(now.plusSeconds(300)) || responseTime.isBefore(now.minusSeconds(300))) {
|
||||
throw new SAMLResponseVerificationFailedException();
|
||||
}
|
||||
}
|
||||
|
||||
// Validate assertion timestamps
|
||||
for (Assertion assertion : samlResponse.getAssertions()) {
|
||||
// Check NotBefore
|
||||
if (assertion.getConditions() != null && assertion.getConditions().getNotBefore() != null) {
|
||||
if (now.isBefore(assertion.getConditions().getNotBefore())) {
|
||||
throw new SAMLResponseVerificationFailedException();
|
||||
}
|
||||
}
|
||||
|
||||
// Check NotOnOrAfter
|
||||
if (assertion.getConditions() != null && assertion.getConditions().getNotOnOrAfter() != null) {
|
||||
if (now.isAfter(assertion.getConditions().getNotOnOrAfter())) {
|
||||
throw new SAMLResponseVerificationFailedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String handleCallback(Main main, TenantIdentifier tenantIdentifier, Storage storage, String samlResponse, String relayState)
|
||||
throws StorageQueryException, XMLParserException, IOException, UnmarshallingException,
|
||||
CertificateException, InvalidRelayStateException, SAMLResponseVerificationFailedException,
|
||||
InvalidClientException, IDPInitiatedLoginDisallowedException, TenantOrAppNotFoundException,
|
||||
FeatureNotEnabledException {
|
||||
checkForSAMLFeature(tenantIdentifier.toAppIdentifier(), main);
|
||||
|
||||
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
|
||||
CoreConfig config = Config.getConfig(tenantIdentifier, main);
|
||||
|
||||
SAMLClient client = null;
|
||||
Response response = parseSamlResponse(samlResponse);
|
||||
String state = null;
|
||||
String redirectURI = null;
|
||||
|
||||
if (relayState != null && !relayState.isEmpty()) {
|
||||
// sp initiated
|
||||
var relayStateInfo = samlStorage.getRelayStateInfo(tenantIdentifier, relayState);
|
||||
if (relayStateInfo == null) {
|
||||
throw new InvalidRelayStateException();
|
||||
}
|
||||
|
||||
String clientId = relayStateInfo.clientId;
|
||||
client = samlStorage.getSAMLClient(tenantIdentifier, clientId);
|
||||
state = relayStateInfo.state;
|
||||
redirectURI = relayStateInfo.redirectURI;
|
||||
} else {
|
||||
// idp initiated
|
||||
String idpEntityId = response.getIssuer().getValue();
|
||||
client = samlStorage.getSAMLClientByIDPEntityId(tenantIdentifier, idpEntityId);
|
||||
redirectURI = client.defaultRedirectURI;
|
||||
|
||||
if (!client.allowIDPInitiatedLogin) {
|
||||
throw new IDPInitiatedLoginDisallowedException();
|
||||
}
|
||||
}
|
||||
|
||||
if (client == null) {
|
||||
throw new InvalidClientException();
|
||||
}
|
||||
|
||||
// SAML verification
|
||||
X509Certificate idpSigningCertificate = getCertificateFromString(client.idpSigningCertificate);
|
||||
try {
|
||||
verifySamlResponseSignature(response, idpSigningCertificate);
|
||||
} catch (SignatureException e) {
|
||||
throw new SAMLResponseVerificationFailedException();
|
||||
}
|
||||
validateSamlResponseTimestamps(response);
|
||||
validateSamlResponseAudience(response, config.getSAMLSPEntityID());
|
||||
|
||||
var claims = extractAllClaims(response);
|
||||
|
||||
String code = UUID.randomUUID().toString();
|
||||
samlStorage.saveSAMLClaims(tenantIdentifier, client.clientId, code, claims, config.getSAMLClaimsValidity());
|
||||
|
||||
try {
|
||||
java.net.URI uri = new java.net.URI(redirectURI);
|
||||
String query = uri.getQuery();
|
||||
StringBuilder newQuery = new StringBuilder();
|
||||
if (query != null && !query.isEmpty()) {
|
||||
newQuery.append(query).append("&");
|
||||
}
|
||||
newQuery.append("code=").append(java.net.URLEncoder.encode(code, java.nio.charset.StandardCharsets.UTF_8));
|
||||
if (state != null) {
|
||||
newQuery.append("&state=").append(java.net.URLEncoder.encode(state, java.nio.charset.StandardCharsets.UTF_8));
|
||||
}
|
||||
java.net.URI newUri = new java.net.URI(
|
||||
uri.getScheme(),
|
||||
uri.getAuthority(),
|
||||
uri.getPath(),
|
||||
newQuery.toString(),
|
||||
uri.getFragment()
|
||||
);
|
||||
return newUri.toString();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalStateException("should never happen", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateSamlResponseAudience(Response samlResponse, String expectedAudience)
|
||||
throws SAMLResponseVerificationFailedException {
|
||||
boolean audienceMatched = false;
|
||||
|
||||
for (Assertion assertion : samlResponse.getAssertions()) {
|
||||
Conditions conditions = assertion.getConditions();
|
||||
if (conditions == null) {
|
||||
continue;
|
||||
}
|
||||
java.util.List<AudienceRestriction> restrictions = conditions.getAudienceRestrictions();
|
||||
if (restrictions == null || restrictions.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (AudienceRestriction ar : restrictions) {
|
||||
java.util.List<Audience> audiences = ar.getAudiences();
|
||||
if (audiences == null || audiences.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (Audience aud : audiences) {
|
||||
if (expectedAudience.equals(aud.getURI())) {
|
||||
audienceMatched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (audienceMatched) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (audienceMatched) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!audienceMatched) {
|
||||
throw new SAMLResponseVerificationFailedException();
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonObject extractAllClaims(Response samlResponse) {
|
||||
JsonObject claims = new JsonObject();
|
||||
|
||||
for (Assertion assertion : samlResponse.getAssertions()) {
|
||||
// Extract NameID as a claim
|
||||
Subject subject = assertion.getSubject();
|
||||
if (subject != null && subject.getNameID() != null) {
|
||||
String nameId = subject.getNameID().getValue();
|
||||
String nameIdFormat = subject.getNameID().getFormat();
|
||||
JsonArray nameIdArr = new JsonArray();
|
||||
nameIdArr.add(nameId);
|
||||
claims.add("NameID", nameIdArr);
|
||||
if (nameIdFormat != null) {
|
||||
JsonArray nameIdFormatArr = new JsonArray();
|
||||
nameIdFormatArr.add(nameIdFormat);
|
||||
claims.add("NameIDFormat", nameIdFormatArr);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract all attributes from AttributeStatements
|
||||
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
|
||||
for (Attribute attribute : attributeStatement.getAttributes()) {
|
||||
String attributeName = attribute.getName();
|
||||
JsonArray attributeValues = new JsonArray();
|
||||
|
||||
for (XMLObject attributeValue : attribute.getAttributeValues()) {
|
||||
if (attributeValue instanceof org.opensaml.saml.saml2.core.AttributeValue) {
|
||||
org.opensaml.saml.saml2.core.AttributeValue attrValue =
|
||||
(org.opensaml.saml.saml2.core.AttributeValue) attributeValue;
|
||||
|
||||
if (attrValue.getDOM() != null) {
|
||||
String value = attrValue.getDOM().getTextContent();
|
||||
if (value != null && !value.trim().isEmpty()) {
|
||||
attributeValues.add(value.trim());
|
||||
}
|
||||
} else if (attrValue.getTextContent() != null) {
|
||||
String value = attrValue.getTextContent();
|
||||
if (!value.trim().isEmpty()) {
|
||||
attributeValues.add(value.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!attributeValues.isEmpty()) {
|
||||
claims.add(attributeName, attributeValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return claims;
|
||||
}
|
||||
|
||||
private static X509Certificate getCertificateFromString(String certString) throws CertificateException {
|
||||
byte[] certBytes = java.util.Base64.getDecoder().decode(certString);
|
||||
java.security.cert.CertificateFactory certFactory =
|
||||
java.security.cert.CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate) certFactory.generateCertificate(
|
||||
new ByteArrayInputStream(certBytes));
|
||||
}
|
||||
|
||||
public static JsonObject getUserInfo(Main main, TenantIdentifier tenantIdentifier, Storage storage, String accessToken, String clientId, boolean isLegacy)
|
||||
throws TenantOrAppNotFoundException, StorageQueryException,
|
||||
StorageTransactionLogicException, InvalidCodeException, FeatureNotEnabledException {
|
||||
|
||||
checkForSAMLFeature(tenantIdentifier.toAppIdentifier(), main);
|
||||
|
||||
SAMLStorage samlStorage = StorageUtils.getSAMLStorage(storage);
|
||||
|
||||
SAMLClaimsInfo claimsInfo = samlStorage.getSAMLClaimsAndRemoveCode(tenantIdentifier, accessToken);
|
||||
if (claimsInfo == null) {
|
||||
throw new InvalidCodeException();
|
||||
}
|
||||
|
||||
if (clientId != null) {
|
||||
if (!clientId.equals(claimsInfo.clientId)) {
|
||||
throw new InvalidCodeException();
|
||||
}
|
||||
}
|
||||
|
||||
String sub = null;
|
||||
String email = null;
|
||||
|
||||
JsonObject claims = claimsInfo.claims;
|
||||
|
||||
if (claims.has("NameID")) {
|
||||
sub = claims.getAsJsonArray("NameID").get(0).getAsString();
|
||||
} else if (claims.has("http://schemas.microsoft.com/identity/claims/objectidentifier")) {
|
||||
sub = claims.getAsJsonArray("http://schemas.microsoft.com/identity/claims/objectidentifier")
|
||||
.get(0).getAsString();
|
||||
} else if (claims.has("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")) {
|
||||
sub = claims.getAsJsonArray("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")
|
||||
.get(0).getAsString();
|
||||
}
|
||||
|
||||
if (claims.has("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")) {
|
||||
email = claims.getAsJsonArray("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")
|
||||
.get(0).getAsString();
|
||||
} else if (claims.has("NameID")) {
|
||||
String nameIdValue = claims.getAsJsonArray("NameID").get(0).getAsString();
|
||||
if (nameIdValue.contains("@")) {
|
||||
email = nameIdValue;
|
||||
}
|
||||
}
|
||||
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.add("claims", claims);
|
||||
payload.addProperty(isLegacy ? "id" : "sub", sub);
|
||||
payload.addProperty("email", email);
|
||||
payload.addProperty("aud", claimsInfo.clientId);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
public static String getLegacyACSURL(Main main, AppIdentifier appIdentifier) throws TenantOrAppNotFoundException {
|
||||
CoreConfig config = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main);
|
||||
return config.getSAMLLegacyACSURL();
|
||||
}
|
||||
|
||||
public static String getMetadataXML(Main main, TenantIdentifier tenantIdentifier)
|
||||
throws TenantOrAppNotFoundException, StorageQueryException, FeatureNotEnabledException {
|
||||
checkForSAMLFeature(tenantIdentifier.toAppIdentifier(), main);
|
||||
|
||||
SAMLCertificate certificate = SAMLCertificate.getInstance(tenantIdentifier.toAppIdentifier(), main);
|
||||
CoreConfig config = Config.getConfig(tenantIdentifier, main);
|
||||
String spEntityId = config.getSAMLSPEntityID();
|
||||
try {
|
||||
X509Certificate cert = certificate.getCertificate();
|
||||
String certString = java.util.Base64.getEncoder().encodeToString(cert.getEncoded());
|
||||
|
||||
String validUntil = java.time.format.DateTimeFormatter.ISO_INSTANT.format(cert.getNotAfter().toInstant());
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>");
|
||||
sb.append("<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"")
|
||||
.append(escapeXml(spEntityId)).append("\" validUntil=\"")
|
||||
.append(escapeXml(validUntil)).append("\">");
|
||||
sb.append("<md:SPSSODescriptor protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">");
|
||||
sb.append("<md:KeyDescriptor use=\"signing\">");
|
||||
sb.append("<ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">");
|
||||
sb.append("<ds:X509Data>");
|
||||
sb.append("<ds:X509Certificate>").append(certString).append("</ds:X509Certificate>");
|
||||
sb.append("</ds:X509Data>");
|
||||
sb.append("</ds:KeyInfo>");
|
||||
sb.append("</md:KeyDescriptor>");
|
||||
sb.append("<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>");
|
||||
sb.append("</md:SPSSODescriptor>");
|
||||
sb.append("</md:EntityDescriptor>");
|
||||
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to generate SP metadata", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String escapeXml(String input) {
|
||||
if (input == null) {
|
||||
return "";
|
||||
}
|
||||
String result = input;
|
||||
result = result.replace("&", "&");
|
||||
result = result.replace("\"", """);
|
||||
result = result.replace("<", "<");
|
||||
result = result.replace(">", ">");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0 (the
|
||||
* "License") as published by the Apache Software Foundation.
|
||||
*
|
||||
* You may not use this file except in compliance with the License. You may
|
||||
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package io.supertokens.saml;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.opensaml.core.config.InitializationException;
|
||||
import org.opensaml.core.config.InitializationService;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
|
||||
public class SAMLBootstrap {
|
||||
private static volatile boolean initialized = false;
|
||||
|
||||
private SAMLBootstrap() {}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
synchronized (SAMLBootstrap.class) {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
InitializationService.initialize();
|
||||
initialized = true;
|
||||
} catch (InitializationException e) {
|
||||
throw new RuntimeException("Failed to initialize OpenSAML", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0 (the
|
||||
* "License") as published by the Apache Software Foundation.
|
||||
*
|
||||
* You may not use this file except in compliance with the License. You may
|
||||
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package io.supertokens.saml;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||
import org.bouncycastle.cert.CertIOException;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
|
||||
import io.supertokens.Main;
|
||||
import io.supertokens.ResourceDistributor;
|
||||
import io.supertokens.output.Logging;
|
||||
import io.supertokens.pluginInterface.KeyValueInfo;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
|
||||
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
|
||||
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
|
||||
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
|
||||
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
|
||||
import io.supertokens.pluginInterface.sqlStorage.SQLStorage;
|
||||
import io.supertokens.storageLayer.StorageLayer;
|
||||
|
||||
public class SAMLCertificate extends ResourceDistributor.SingletonResource {
|
||||
private static final String RESOURCE_KEY = "io.supertokens.saml.SAMLCertificate";
|
||||
private final Main main;
|
||||
private final AppIdentifier appIdentifier;
|
||||
|
||||
private static final String SAML_KEY_PAIR_NAME = "saml_key_pair";
|
||||
private static final String SAML_CERTIFICATE_NAME = "saml_certificate";
|
||||
|
||||
private KeyPair spKeyPair = null;
|
||||
private X509Certificate spCertificate = null;
|
||||
|
||||
private SAMLCertificate(AppIdentifier appIdentifier, Main main) throws
|
||||
TenantOrAppNotFoundException {
|
||||
this.main = main;
|
||||
this.appIdentifier = appIdentifier;
|
||||
try {
|
||||
if (!Main.isTesting) {
|
||||
// Creation of new certificate is slow, not really necessary to create one for each test
|
||||
this.getCertificate();
|
||||
}
|
||||
} catch (StorageQueryException | TenantOrAppNotFoundException e) {
|
||||
Logging.error(main, appIdentifier.getAsPublicTenantIdentifier(), "Error while fetching SAML key and certificate",
|
||||
false, e);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized X509Certificate getCertificate()
|
||||
throws StorageQueryException, TenantOrAppNotFoundException {
|
||||
if (this.spCertificate == null || this.spCertificate.getNotAfter().before(new Date())) {
|
||||
maybeGenerateNewCertificateAndUpdateInDb();
|
||||
}
|
||||
|
||||
return this.spCertificate;
|
||||
}
|
||||
|
||||
private void maybeGenerateNewCertificateAndUpdateInDb() throws TenantOrAppNotFoundException {
|
||||
SQLStorage storage = (SQLStorage) StorageLayer.getStorage(
|
||||
this.appIdentifier.getAsPublicTenantIdentifier(), main);
|
||||
|
||||
try {
|
||||
storage.startTransaction(con -> {
|
||||
KeyValueInfo keyPairInfo = storage.getKeyValue_Transaction(this.appIdentifier.getAsPublicTenantIdentifier(), con, SAML_KEY_PAIR_NAME);
|
||||
KeyValueInfo certInfo = storage.getKeyValue_Transaction(this.appIdentifier.getAsPublicTenantIdentifier(), con, SAML_CERTIFICATE_NAME);
|
||||
|
||||
if (keyPairInfo == null || certInfo == null) {
|
||||
try {
|
||||
generateNewCertificate();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
String keyPairStr = serializeKeyPair(spKeyPair);
|
||||
String certStr = serializeCertificate(spCertificate);
|
||||
keyPairInfo = new KeyValueInfo(keyPairStr);
|
||||
certInfo = new KeyValueInfo(certStr);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to serialize key pair or certificate", e);
|
||||
}
|
||||
storage.setKeyValue_Transaction(this.appIdentifier.getAsPublicTenantIdentifier(), con, SAML_KEY_PAIR_NAME, keyPairInfo);
|
||||
storage.setKeyValue_Transaction(this.appIdentifier.getAsPublicTenantIdentifier(), con, SAML_CERTIFICATE_NAME, certInfo);
|
||||
}
|
||||
|
||||
String keyPairStr = keyPairInfo.value;
|
||||
String certStr = certInfo.value;
|
||||
|
||||
try {
|
||||
this.spKeyPair = deserializeKeyPair(keyPairStr);
|
||||
this.spCertificate = deserializeCertificate(certStr);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to deserialize key pair or certificate", e);
|
||||
}
|
||||
|
||||
// If the certificate has expired, generate and persist a new one
|
||||
if (this.spCertificate.getNotAfter().before(new Date())) {
|
||||
try {
|
||||
generateNewCertificate();
|
||||
String newKeyPairStr = serializeKeyPair(spKeyPair);
|
||||
String newCertStr = serializeCertificate(spCertificate);
|
||||
KeyValueInfo newKeyPairInfo = new KeyValueInfo(newKeyPairStr);
|
||||
KeyValueInfo newCertInfo = new KeyValueInfo(newCertStr);
|
||||
storage.setKeyValue_Transaction(this.appIdentifier.getAsPublicTenantIdentifier(), con, SAML_KEY_PAIR_NAME, newKeyPairInfo);
|
||||
storage.setKeyValue_Transaction(this.appIdentifier.getAsPublicTenantIdentifier(), con, SAML_CERTIFICATE_NAME, newCertInfo);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to regenerate expired certificate", e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
} catch (StorageTransactionLogicException | StorageQueryException e) {
|
||||
throw new RuntimeException("Storage error", e);
|
||||
}
|
||||
}
|
||||
|
||||
void generateNewCertificate()
|
||||
throws NoSuchAlgorithmException, CertificateException, OperatorCreationException, CertIOException {
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen.initialize(4096);
|
||||
spKeyPair = keyGen.generateKeyPair();
|
||||
spCertificate = generateSelfSignedCertificate();
|
||||
}
|
||||
|
||||
private X509Certificate generateSelfSignedCertificate()
|
||||
throws CertIOException, OperatorCreationException, CertificateException {
|
||||
// Create a production-ready self-signed X.509 certificate using BouncyCastle
|
||||
Date notBefore = new Date();
|
||||
Date notAfter = new Date(notBefore.getTime() + 10 * 365L * 24 * 60 * 60 * 1000); // 10 year validity
|
||||
|
||||
// Create the certificate subject and issuer (same for self-signed)
|
||||
X500Name subject = new X500Name("CN=SAML-SP, O=SuperTokens, C=US");
|
||||
X500Name issuer = subject; // Self-signed
|
||||
|
||||
// Generate a random serial number (128 bits for good uniqueness)
|
||||
SecureRandom random = new SecureRandom();
|
||||
java.math.BigInteger serialNumber = new java.math.BigInteger(128, random);
|
||||
|
||||
// Create the certificate builder
|
||||
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
||||
issuer,
|
||||
serialNumber,
|
||||
notBefore,
|
||||
notAfter,
|
||||
subject,
|
||||
spKeyPair.getPublic()
|
||||
);
|
||||
|
||||
// Add extensions for proper SAML usage
|
||||
// Key Usage: digitalSignature and keyEncipherment
|
||||
KeyUsage keyUsage = new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment);
|
||||
certBuilder.addExtension(Extension.keyUsage, true, keyUsage);
|
||||
|
||||
// Basic Constraints: not a CA
|
||||
BasicConstraints basicConstraints = new BasicConstraints(false);
|
||||
certBuilder.addExtension(Extension.basicConstraints, true, basicConstraints);
|
||||
|
||||
// Create the content signer
|
||||
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA")
|
||||
.build(spKeyPair.getPrivate());
|
||||
|
||||
// Build the certificate
|
||||
X509CertificateHolder certHolder = certBuilder.build(contentSigner);
|
||||
|
||||
// Convert to standard X509Certificate
|
||||
JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
|
||||
return converter.getCertificate(certHolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a KeyPair to a Base64 encoded string format
|
||||
*/
|
||||
private String serializeKeyPair(KeyPair keyPair) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
// Write private key
|
||||
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
|
||||
baos.write(Base64.getEncoder().encode(privateKeyBytes));
|
||||
baos.write('\n');
|
||||
|
||||
// Write public key
|
||||
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
|
||||
baos.write(Base64.getEncoder().encode(publicKeyBytes));
|
||||
|
||||
return baos.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a KeyPair from a Base64 encoded string format
|
||||
*/
|
||||
private KeyPair deserializeKeyPair(String keyPairStr) throws Exception {
|
||||
String[] parts = keyPairStr.split("\n");
|
||||
if (parts.length != 2) {
|
||||
throw new IllegalArgumentException("Invalid key pair string format");
|
||||
}
|
||||
|
||||
// Decode private key
|
||||
byte[] privateKeyBytes = Base64.getDecoder().decode(parts[0]);
|
||||
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
|
||||
|
||||
// Decode public key
|
||||
byte[] publicKeyBytes = Base64.getDecoder().decode(parts[1]);
|
||||
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
|
||||
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
|
||||
|
||||
return new KeyPair(publicKey, privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes an X509Certificate to a Base64 encoded string format
|
||||
*/
|
||||
private String serializeCertificate(X509Certificate certificate) throws IOException {
|
||||
try {
|
||||
byte[] certBytes = certificate.getEncoded();
|
||||
return Base64.getEncoder().encodeToString(certBytes);
|
||||
} catch (CertificateException e) {
|
||||
throw new IOException("Failed to encode certificate", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes an X509Certificate from a Base64 encoded string format
|
||||
*/
|
||||
private X509Certificate deserializeCertificate(String certStr) throws Exception {
|
||||
try {
|
||||
byte[] certBytes = Base64.getDecoder().decode(certStr);
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(certBytes);
|
||||
return (X509Certificate) certFactory.generateCertificate(bais);
|
||||
} catch (CertificateException e) {
|
||||
throw new Exception("Failed to decode certificate", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static SAMLCertificate getInstance(AppIdentifier appIdentifier, Main main)
|
||||
throws TenantOrAppNotFoundException {
|
||||
return (SAMLCertificate) main.getResourceDistributor()
|
||||
.getResource(appIdentifier, RESOURCE_KEY);
|
||||
}
|
||||
|
||||
public static void loadForAllTenants(Main main, List<AppIdentifier> apps,
|
||||
List<TenantIdentifier> tenantsThatChanged) {
|
||||
try {
|
||||
main.getResourceDistributor().withResourceDistributorLock(() -> {
|
||||
Map<ResourceDistributor.KeyClass, ResourceDistributor.SingletonResource> existingResources =
|
||||
main.getResourceDistributor()
|
||||
.getAllResourcesWithResourceKey(RESOURCE_KEY);
|
||||
main.getResourceDistributor().clearAllResourcesWithResourceKey(RESOURCE_KEY);
|
||||
for (AppIdentifier app : apps) {
|
||||
ResourceDistributor.SingletonResource resource = existingResources.get(
|
||||
new ResourceDistributor.KeyClass(app, RESOURCE_KEY));
|
||||
if (resource != null && !tenantsThatChanged.contains(app.getAsPublicTenantIdentifier())) {
|
||||
main.getResourceDistributor().setResource(app, RESOURCE_KEY,
|
||||
resource);
|
||||
} else {
|
||||
try {
|
||||
main.getResourceDistributor()
|
||||
.setResource(app, RESOURCE_KEY,
|
||||
new SAMLCertificate(app, main));
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
Logging.error(main, app.getAsPublicTenantIdentifier(), e.getMessage(), false);
|
||||
// continue loading other resources
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} catch (ResourceDistributor.FuncException e) {
|
||||
throw new IllegalStateException("should never happen", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package io.supertokens.saml.exceptions;
|
||||
|
||||
public class IDPInitiatedLoginDisallowedException extends Exception {
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0 (the
|
||||
* "License") as published by the Apache Software Foundation.
|
||||
*
|
||||
* You may not use this file except in compliance with the License. You may
|
||||
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package io.supertokens.saml.exceptions;
|
||||
|
||||
public class InvalidClientException extends Exception {
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package io.supertokens.saml.exceptions;
|
||||
|
||||
public class InvalidCodeException extends Exception {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package io.supertokens.saml.exceptions;
|
||||
|
||||
public class InvalidRelayStateException extends Exception {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0 (the
|
||||
* "License") as published by the Apache Software Foundation.
|
||||
*
|
||||
* You may not use this file except in compliance with the License. You may
|
||||
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package io.supertokens.saml.exceptions;
|
||||
|
||||
public class MalformedSAMLMetadataXMLException extends Exception {
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package io.supertokens.saml.exceptions;
|
||||
|
||||
public class SAMLResponseVerificationFailedException extends Exception {
|
||||
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ package io.supertokens.session;
|
|||
import com.google.gson.JsonObject;
|
||||
import io.supertokens.Main;
|
||||
import io.supertokens.ProcessState;
|
||||
import io.supertokens.ResourceDistributor;
|
||||
import io.supertokens.config.Config;
|
||||
import io.supertokens.config.CoreConfig;
|
||||
import io.supertokens.exceptions.AccessTokenPayloadError;
|
||||
|
|
@ -100,7 +101,7 @@ public class Session {
|
|||
Storage storage = StorageLayer.getStorage(main);
|
||||
try {
|
||||
return createNewSession(
|
||||
new TenantIdentifier(null, null, null), storage, main,
|
||||
ResourceDistributor.getAppForTesting(), storage, main,
|
||||
recipeUserId, userDataInJWT, userDataInDatabase, false, AccessToken.getLatestVersion(), false);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -120,7 +121,7 @@ public class Session {
|
|||
Storage storage = StorageLayer.getStorage(main);
|
||||
try {
|
||||
return createNewSession(
|
||||
new TenantIdentifier(null, null, null), storage, main,
|
||||
ResourceDistributor.getAppForTesting(), storage, main,
|
||||
recipeUserId, userDataInJWT, userDataInDatabase, enableAntiCsrf, version, useStaticKey);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -200,7 +201,7 @@ public class Session {
|
|||
InvalidKeyException, JWT.JWTException,
|
||||
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError, TryRefreshTokenException {
|
||||
try {
|
||||
return regenerateToken(new AppIdentifier(null, null), main, token, userDataInJWT);
|
||||
return regenerateToken(ResourceDistributor.getAppForTesting().toAppIdentifier(), main, token, userDataInJWT);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
|
@ -326,7 +327,7 @@ public class Session {
|
|||
StorageTransactionLogicException, TryRefreshTokenException, UnauthorisedException,
|
||||
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError {
|
||||
try {
|
||||
return getSession(new AppIdentifier(null, null), main, token, antiCsrfToken, enableAntiCsrf,
|
||||
return getSession(ResourceDistributor.getAppForTesting().toAppIdentifier(), main, token, antiCsrfToken, enableAntiCsrf,
|
||||
doAntiCsrfCheck, checkDatabase);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -532,7 +533,7 @@ public class Session {
|
|||
UnauthorisedException, StorageQueryException, TokenTheftDetectedException,
|
||||
UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError {
|
||||
try {
|
||||
return refreshSession(new AppIdentifier(null, null), main, refreshToken, antiCsrfToken,
|
||||
return refreshSession(ResourceDistributor.getAppForTesting().toAppIdentifier(), main, refreshToken, antiCsrfToken,
|
||||
enableAntiCsrf, accessTokenVersion, null);
|
||||
} catch (TenantOrAppNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
@ -768,7 +769,7 @@ public class Session {
|
|||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return revokeSessionUsingSessionHandles(main,
|
||||
new AppIdentifier(null, null), storage,
|
||||
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage,
|
||||
sessionHandles);
|
||||
}
|
||||
|
||||
|
|
@ -860,7 +861,7 @@ public class Session {
|
|||
public static String[] revokeAllSessionsForUser(Main main, String userId) throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return revokeAllSessionsForUser(main,
|
||||
new AppIdentifier(null, null), storage, userId, true);
|
||||
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId, true);
|
||||
}
|
||||
|
||||
public static String[] revokeAllSessionsForUser(Main main, AppIdentifier appIdentifier,
|
||||
|
|
@ -886,7 +887,7 @@ public class Session {
|
|||
throws StorageQueryException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getAllNonExpiredSessionHandlesForUser(main,
|
||||
new AppIdentifier(null, null), storage, userId, true);
|
||||
ResourceDistributor.getAppForTesting().toAppIdentifier(), storage, userId, true);
|
||||
}
|
||||
|
||||
public static String[] getAllNonExpiredSessionHandlesForUser(
|
||||
|
|
@ -957,7 +958,7 @@ public class Session {
|
|||
throws StorageQueryException, UnauthorisedException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getSessionData(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
sessionHandle);
|
||||
}
|
||||
|
||||
|
|
@ -978,7 +979,7 @@ public class Session {
|
|||
throws StorageQueryException, UnauthorisedException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getJWTData(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
sessionHandle);
|
||||
}
|
||||
|
||||
|
|
@ -998,7 +999,7 @@ public class Session {
|
|||
throws StorageQueryException, UnauthorisedException {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
return getSession(
|
||||
new TenantIdentifier(null, null, null), storage,
|
||||
ResourceDistributor.getAppForTesting(), storage,
|
||||
sessionHandle);
|
||||
}
|
||||
|
||||
|
|
@ -1028,7 +1029,7 @@ public class Session {
|
|||
AccessToken.VERSION version)
|
||||
throws StorageQueryException, UnauthorisedException, AccessTokenPayloadError {
|
||||
Storage storage = StorageLayer.getStorage(main);
|
||||
updateSession(new TenantIdentifier(null, null, null), storage,
|
||||
updateSession(ResourceDistributor.getAppForTesting(), storage,
|
||||
sessionHandle, sessionData, jwtData, version);
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue