first commit.

This commit is contained in:
hailin 2025-05-14 22:47:16 +08:00
commit b6bb6585d8
2565 changed files with 454830 additions and 0 deletions

491
Dockerfile Normal file
View File

@ -0,0 +1,491 @@
# syntax=docker/dockerfile:1.6
FROM golang:1.22.3-alpine3.20 as authbuild
ENV GO111MODULE=on
ENV CGO_ENABLED=0
ENV GOOS=linux
RUN apk add --no-cache make git
WORKDIR /go/src/github.com/supabase/auth
# Pulling dependencies
COPY auth_v2.169.0/Makefile auth_v2.169.0/go.* ./
RUN make deps
# Building stuff
COPY auth_v2.169.0/. ./
# Make sure you change the RELEASE_VERSION value before publishing an image.
RUN RELEASE_VERSION=1.22.3 make build
# Base stage for shared environment setup
FROM node:20-alpine3.20 as s3base
RUN apk add --no-cache g++ make python3
WORKDIR /app
COPY storage_v1.19.1/package.json storage_v1.19.1/package-lock.json ./
# Dependencies stage - install and cache all dependencies
FROM s3base as dependencies
RUN npm ci
# Cache the installed node_modules for later stages
RUN cp -R node_modules /node_modules_cache
# Build stage - use cached node_modules for building the application
FROM s3base as s3build
COPY --from=dependencies /node_modules_cache ./node_modules
COPY storage_v1.19.1/. .
RUN npm run build
# Production dependencies stage - use npm cache to install only production dependencies
FROM s3base as production-deps
COPY --from=dependencies /node_modules_cache ./node_modules
RUN npm ci --production
#EXPOSE 5000
#CMD ["node", "dist/start/server.js"]
# Always use alpine:3 so the latest version is used. This will keep CA certs more up to date.
#FROM alpine:3
FROM nvcr.io/nvidia/tritonserver:24.04-py3-min as base
RUN mkdir -p /storage-api
#RUN adduser -D -u 1000 supabase
#RUN apk add --no-cache ca-certificates
# 创建用户Ubuntu方式
RUN useradd -m -u 1000 supabase
# 安装 ca-certificatesUbuntu方式
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
COPY --from=authbuild /go/src/github.com/supabase/auth/auth /usr/local/bin/auth
COPY --from=authbuild /go/src/github.com/supabase/auth/migrations /usr/local/etc/auth/migrations/
RUN ln -s /usr/local/bin/auth /usr/local/bin/gotrue
ENV GOTRUE_DB_MIGRATIONS_PATH /usr/local/etc/auth/migrations
#USER supabase
#CMD ["auth"]
ARG VERSION
ENV VERSION=$VERSION
COPY storage_v1.19.1/migrations migrations
# Copy production node_modules from the production dependencies stage
COPY --from=production-deps /app/node_modules node_modules
# Copy build artifacts from the build stage
COPY --from=s3build /app/dist dist
#----------------------------------------------------------------- Postgrest --------------------------------
RUN apt-get update -y \
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY postgrest_v12.2.8/postgrest /usr/bin/postgrest
RUN chmod +x /usr/bin/postgrest
#EXPOSE 3000
#USER 1000
# Use the array form to avoid running the command using bash, which does not handle `SIGTERM` properly.
# See https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop
#CMD ["postgrest"]
#========================================================================== PostgreSQL =============================================================
ARG postgresql_major=15
ARG postgresql_release=${postgresql_major}.1
# Bump default build arg to build a package from source
# Bump vars.yml to specify runtime package version
ARG sfcgal_release=1.3.10
ARG postgis_release=3.3.2
ARG pgrouting_release=3.4.1
ARG pgtap_release=1.2.0
ARG pg_cron_release=1.6.2
ARG pgaudit_release=1.7.0
ARG pgjwt_release=9742dab1b2f297ad3811120db7b21451bca2d3c9
ARG pgsql_http_release=1.5.0
ARG plpgsql_check_release=2.2.5
ARG pg_safeupdate_release=1.4
ARG timescaledb_release=2.9.1
ARG wal2json_release=2_5
ARG pljava_release=1.6.4
ARG plv8_release=3.1.5
ARG pg_plan_filter_release=5081a7b5cb890876e67d8e7486b6a64c38c9a492
ARG pg_net_release=0.7.1
ARG rum_release=1.3.13
ARG pg_hashids_release=cd0e1b31d52b394a0df64079406a14a4f7387cd6
ARG libsodium_release=1.0.18
ARG pgsodium_release=3.1.6
ARG pg_graphql_release=1.5.11
ARG pg_stat_monitor_release=1.1.1
ARG pg_jsonschema_release=0.1.4
ARG pg_repack_release=1.4.8
ARG vault_release=0.2.8
ARG groonga_release=12.0.8
ARG pgroonga_release=2.4.0
ARG wrappers_release=0.3.0
ARG hypopg_release=1.3.1
ARG pgvector_release=0.4.0
ARG pg_tle_release=1.3.2
ARG index_advisor_release=0.2.0
ARG supautils_release=2.2.0
ARG wal_g_release=2.0.1
#FROM ubuntu:focal as base
#FROM nvcr.io/nvidia/tritonserver:24.04-py3-min as base
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update -y && apt install -y \
curl \
gnupg \
lsb-release \
software-properties-common \
wget \
sudo \
&& apt clean
RUN adduser --system --home /var/lib/postgresql --no-create-home --shell /bin/bash --group --gecos "PostgreSQL administrator" postgres
RUN adduser --system --no-create-home --shell /bin/bash --group wal-g
RUN curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux \
--init none \
--no-confirm \
--extra-conf "substituters = https://cache.nixos.org https://nix-postgres-artifacts.s3.amazonaws.com" \
--extra-conf "trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI=% cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
ENV PATH="${PATH}:/nix/var/nix/profiles/default/bin"
COPY postgres_15.8.1.044/. /nixpg
WORKDIR /nixpg
RUN nix profile install .#psql_15/bin
WORKDIR /
RUN mkdir -p /usr/lib/postgresql/bin \
/usr/lib/postgresql/share/postgresql \
/usr/share/postgresql \
/var/lib/postgresql \
&& chown -R postgres:postgres /usr/lib/postgresql \
&& chown -R postgres:postgres /var/lib/postgresql \
&& chown -R postgres:postgres /usr/share/postgresql
# Create symbolic links
RUN ln -sf /nix/var/nix/profiles/default/bin/* /usr/lib/postgresql/bin/ \
&& ln -sf /nix/var/nix/profiles/default/bin/* /usr/bin/ \
&& chown -R postgres:postgres /usr/bin
# Create symbolic links for PostgreSQL shares
RUN ln -sf /nix/var/nix/profiles/default/share/postgresql/* /usr/lib/postgresql/share/postgresql/
RUN ln -sf /nix/var/nix/profiles/default/share/postgresql/* /usr/share/postgresql/
RUN chown -R postgres:postgres /usr/lib/postgresql/share/postgresql/
RUN chown -R postgres:postgres /usr/share/postgresql/
# Create symbolic links for contrib directory
RUN mkdir -p /usr/lib/postgresql/share/postgresql/contrib \
&& find /nix/var/nix/profiles/default/share/postgresql/contrib/ -mindepth 1 -type d -exec sh -c 'for dir do ln -s "$dir" "/usr/lib/postgresql/share/postgresql/contrib/$(basename "$dir")"; done' sh {} + \
&& chown -R postgres:postgres /usr/lib/postgresql/share/postgresql/contrib/
RUN chown -R postgres:postgres /usr/lib/postgresql
RUN ln -sf /usr/lib/postgresql/share/postgresql/timezonesets /usr/share/postgresql/timezonesets
RUN apt-get update && \
apt-get install -y --no-install-recommends tzdata
RUN ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime && \
dpkg-reconfigure --frontend noninteractive tzdata
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
checkinstall \
cmake
ENV PGDATA=/var/lib/postgresql/data
####################
# setup-wal-g.yml
####################
FROM base as walg
ARG wal_g_release
# ADD "https://github.com/wal-g/wal-g/releases/download/v${wal_g_release}/wal-g-pg-ubuntu-20.04-${TARGETARCH}.tar.gz" /tmp/wal-g.tar.gz
RUN arch=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "$TARGETARCH") && \
apt-get update && apt-get install -y --no-install-recommends curl && \
curl -kL "https://github.com/wal-g/wal-g/releases/download/v${wal_g_release}/wal-g-pg-ubuntu-20.04-aarch64.tar.gz" -o /tmp/wal-g.tar.gz && \
tar -xvf /tmp/wal-g.tar.gz -C /tmp && \
rm -rf /tmp/wal-g.tar.gz && \
mv /tmp/wal-g-pg-ubuntu*20.04-aarch64 /tmp/wal-g
# ####################
# # Download gosu for easy step-down from root
# ####################
FROM base as gosu
ARG TARGETARCH
# Install dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gnupg \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Download binary
ARG GOSU_VERSION=1.16
ARG GOSU_GPG_KEY=B42F6819007F00F88E364FD4036A9C25BF357DD4
ADD https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$TARGETARCH \
/usr/local/bin/gosu
ADD https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$TARGETARCH.asc \
/usr/local/bin/gosu.asc
# Verify checksum
RUN gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys $GOSU_GPG_KEY && \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu && \
gpgconf --kill all && \
chmod +x /usr/local/bin/gosu
# ####################
# # Build final image
# ####################
FROM gosu as production
RUN id postgres || (echo "postgres user does not exist" && exit 1)
# # Setup extensions
COPY --from=walg /tmp/wal-g /usr/local/bin/
# # Initialise configs
COPY --chown=postgres:postgres postgres_15.8.1.044/ansible/files/postgresql_config/postgresql.conf.j2 /etc/postgresql/postgresql.conf
COPY --chown=postgres:postgres postgres_15.8.1.044/ansible/files/postgresql_config/pg_hba.conf.j2 /etc/postgresql/pg_hba.conf
COPY --chown=postgres:postgres postgres_15.8.1.044/ansible/files/postgresql_config/pg_ident.conf.j2 /etc/postgresql/pg_ident.conf
COPY --chown=postgres:postgres postgres_15.8.1.044/ansible/files/postgresql_config/postgresql-stdout-log.conf /etc/postgresql/logging.conf
COPY --chown=postgres:postgres postgres_15.8.1.044/ansible/files/postgresql_config/supautils.conf.j2 /etc/postgresql-custom/supautils.conf
COPY --chown=postgres:postgres postgres_15.8.1.044/ansible/files/postgresql_extension_custom_scripts /etc/postgresql-custom/extension-custom-scripts
COPY --chown=postgres:postgres postgres_15.8.1.044/ansible/files/pgsodium_getkey_urandom.sh.j2 /usr/lib/postgresql/bin/pgsodium_getkey.sh
COPY --chown=postgres:postgres postgres_15.8.1.044/ansible/files/postgresql_config/custom_read_replica.conf.j2 /etc/postgresql-custom/read-replica.conf
COPY --chown=postgres:postgres postgres_15.8.1.044/ansible/files/postgresql_config/custom_walg.conf.j2 /etc/postgresql-custom/wal-g.conf
COPY --chown=postgres:postgres postgres_15.8.1.044/ansible/files/walg_helper_scripts/wal_fetch.sh /home/postgres/wal_fetch.sh
COPY postgres_15.8.1.044/ansible/files/walg_helper_scripts/wal_change_ownership.sh /root/wal_change_ownership.sh
RUN sed -i \
-e "s|#unix_socket_directories = '/tmp'|unix_socket_directories = '/var/run/postgresql'|g" \
-e "s|#session_preload_libraries = ''|session_preload_libraries = 'supautils'|g" \
-e "s|#include = '/etc/postgresql-custom/supautils.conf'|include = '/etc/postgresql-custom/supautils.conf'|g" \
-e "s|#include = '/etc/postgresql-custom/wal-g.conf'|include = '/etc/postgresql-custom/wal-g.conf'|g" /etc/postgresql/postgresql.conf && \
echo "cron.database_name = 'postgres'" >> /etc/postgresql/postgresql.conf && \
#echo "pljava.libjvm_location = '/usr/lib/jvm/java-11-openjdk-${TARGETARCH}/lib/server/libjvm.so'" >> /etc/postgresql/postgresql.conf && \
echo "pgsodium.getkey_script= '/usr/lib/postgresql/bin/pgsodium_getkey.sh'" >> /etc/postgresql/postgresql.conf && \
echo 'auto_explain.log_min_duration = 10s' >> /etc/postgresql/postgresql.conf && \
usermod -aG postgres wal-g && \
mkdir -p /etc/postgresql-custom && \
chown postgres:postgres /etc/postgresql-custom
# # Include schema migrations
COPY postgres_15.8.1.044/migrations/db /docker-entrypoint-initdb.d/
COPY postgres_15.8.1.044/ansible/files/pgbouncer_config/pgbouncer_auth_schema.sql /docker-entrypoint-initdb.d/init-scripts/00-schema.sql
COPY postgres_15.8.1.044/ansible/files/stat_extension.sql /docker-entrypoint-initdb.d/migrations/00-extension.sql
# # Add upstream entrypoint script
COPY --from=gosu /usr/local/bin/gosu /usr/local/bin/gosu
ADD --chmod=0755 \
https://github.com/docker-library/postgres/raw/master/15/bullseye/docker-entrypoint.sh \
/usr/local/bin/
RUN mkdir -p /var/run/postgresql && chown postgres:postgres /var/run/postgresql
ENTRYPOINT ["docker-entrypoint.sh"]
HEALTHCHECK --interval=2s --timeout=2s --retries=10 CMD pg_isready -U postgres -h localhost
STOPSIGNAL SIGINT
EXPOSE 5432
ENV POSTGRES_HOST=/var/run/postgresql
ENV POSTGRES_USER=supabase_admin
ENV POSTGRES_DB=postgres
RUN apt-get update && apt-get install -y --no-install-recommends \
locales \
&& rm -rf /var/lib/apt/lists/* && \
localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \
&& localedef -i C -c -f UTF-8 -A /usr/share/locale/locale.alias C.UTF-8
RUN echo "C.UTF-8 UTF-8" > /etc/locale.gen && echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen && locale-gen
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
ENV LC_CTYPE=C.UTF-8
ENV LC_COLLATE=C.UTF-8
ENV LOCALE_ARCHIVE /usr/lib/locale/locale-archive
#CMD ["postgres", "-D", "/etc/postgresql"]
#=================================================== kong =====================================================================
ARG ASSET=ce
ENV ASSET $ASSET
ARG EE_PORTS
COPY docker-kong_v2.8.1/ubuntu/kong.deb /tmp/kong.deb
ARG KONG_VERSION=2.8.1
ENV KONG_VERSION $KONG_VERSION
ARG KONG_AMD64_SHA="10d12d23e5890414d666663094d51a42de41f8a9806fbc0baaf9ac4d37794361"
ARG KONG_ARM64_SHA="61c13219ef64dac9aeae5ae775411e8cfcd406f068cf3e75d463f916ae6513cb"
# hadolint ignore=DL3015
RUN set -ex; \
arch=$(dpkg --print-architecture); \
case "${arch}" in \
amd64) KONG_SHA256=$KONG_AMD64_SHA ;; \
arm64) KONG_SHA256=$KONG_ARM64_SHA ;; \
esac; \
apt-get update \
&& if [ "$ASSET" = "ce" ] ; then \
apt-get install -y curl \
&& UBUNTU_CODENAME=focal \
&& KONG_REPO=$(echo ${KONG_VERSION%.*} | sed 's/\.//') \
&& curl -fL https://packages.konghq.com/public/gateway-$KONG_REPO/deb/ubuntu/pool/$UBUNTU_CODENAME/main/k/ko/kong_$KONG_VERSION/kong_${KONG_VERSION}_$arch.deb -o /tmp/kong.deb \
&& apt-get purge -y curl \
&& echo "$KONG_SHA256 /tmp/kong.deb" | sha256sum -c -; \
else \
# this needs to stay inside this "else" block so that it does not become part of the "official images" builds (https://github.com/docker-library/official-images/pull/11532#issuecomment-996219700)
apt-get upgrade -y ; \
fi; \
apt-get install -y --no-install-recommends unzip git \
# Please update the ubuntu install docs if the below line is changed so that
# end users can properly install Kong along with its required dependencies
# and that our CI does not diverge from our docs.
&& apt install --yes /tmp/kong.deb \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /tmp/kong.deb \
&& chown kong:0 /usr/local/bin/kong \
&& chown -R kong:0 /usr/local/kong \
&& ln -s /usr/local/openresty/bin/resty /usr/local/bin/resty \
&& ln -s /usr/local/openresty/luajit/bin/luajit /usr/local/bin/luajit \
&& ln -s /usr/local/openresty/luajit/bin/luajit /usr/local/bin/lua \
&& ln -s /usr/local/openresty/nginx/sbin/nginx /usr/local/bin/nginx \
&& if [ "$ASSET" = "ce" ] ; then \
kong version ; \
fi
COPY docker-kong_v2.8.1/ubuntu/docker-entrypoint.sh /docker-entrypoint.sh
#USER kong
#ENTRYPOINT ["/docker-entrypoint.sh"]
#EXPOSE 8000 8443 8001 8444 $EE_PORTS
#STOPSIGNAL SIGQUIT
#HEALTHCHECK --interval=10s --timeout=10s --retries=10 CMD kong health
#CMD ["kong", "docker-start"]
ARG CUDA_VERSION=12.5.1
#============================================= sglang ============================================
#FROM nvcr.io/nvidia/tritonserver:24.04-py3-min
ARG BUILD_TYPE=all
#ENV DEBIAN_FRONTEND=noninteractive
# 安装依赖(强制 IPv4
RUN echo 'tzdata tzdata/Areas select Asia' | debconf-set-selections \
&& echo 'tzdata tzdata/Zones/Asia select Shanghai' | debconf-set-selections \
&& apt -o Acquire::ForceIPv4=true update -y \
&& apt -o Acquire::ForceIPv4=true install software-properties-common -y \
&& add-apt-repository ppa:deadsnakes/ppa -y \
&& apt -o Acquire::ForceIPv4=true update \
&& apt -o Acquire::ForceIPv4=true install python3.10 python3.10-dev -y \
&& update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1 \
&& update-alternatives --set python3 /usr/bin/python3.10 \
&& apt -o Acquire::ForceIPv4=true install python3.10-distutils -y \
&& apt -o Acquire::ForceIPv4=true install curl gnupg gnupg wget git sudo libibverbs-dev -y \
&& apt -o Acquire::ForceIPv4=true install -y rdma-core infiniband-diags openssh-server perftest ibverbs-providers libibumad3 libibverbs1 libnl-3-200 libnl-route-3-200 librdmacm1 \
&& curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py \
&& python3 get-pip.py \
&& python3 --version \
&& python3 -m pip --version \
&& rm -rf /var/lib/apt/lists/* \
&& apt clean
# 安装 datamodel_code_generator用于 MiniCPM 模型)
RUN pip3 install datamodel_code_generator
WORKDIR /sgl-workspace
# 拷贝 sglang 源代码并构建包
COPY ./sglang /sgl-workspace/sglang
# 拷贝模型文件(修正方式)
#COPY ./Alibaba/QwQ-32B /root/.cradle/Alibaba/QwQ-32B
ARG CUDA_VERSION
# 安装依赖、安装 sglang、安装 transformers并清理源码
RUN python3 -m pip install --upgrade pip setuptools wheel html5lib six \
&& if [ "$CUDA_VERSION" = "12.1.1" ]; then \
CUINDEX=121; \
elif [ "$CUDA_VERSION" = "12.4.1" ]; then \
CUINDEX=124; \
elif [ "$CUDA_VERSION" = "12.5.1" ]; then \
CUINDEX=124; \
elif [ "$CUDA_VERSION" = "11.8.0" ]; then \
CUINDEX=118; \
python3 -m pip install --no-cache-dir sgl-kernel -i https://docs.sglang.ai/whl/cu118; \
else \
echo "Unsupported CUDA version: $CUDA_VERSION" && exit 1; \
fi \
&& python3 -m pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cu${CUINDEX} \
&& python3 -m pip install --no-cache-dir psutil pyzmq pynvml \
&& cd /sgl-workspace/sglang/python \
&& python3 -m pip install --no-cache-dir '.[srt,openai]' --find-links https://flashinfer.ai/whl/cu${CUINDEX}/torch2.5/flashinfer-python \
&& cd / && rm -rf /sgl-workspace/sglang \
&& python3 -m pip install --no-cache-dir transformers==4.48.3 \
&& python3 -c "import sglang; print('✅ sglang module installed successfully')"
#================================================== PostgreSQL ==============================================================
RUN adduser --system --home /var/lib/postgresql --no-create-home --shell /bin/bash --group --gecos "PostgreSQL administrator" postgres
RUN adduser --system --no-create-home --shell /bin/bash --group wal-g
RUN curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux \
--init none \
--no-confirm \
--extra-conf "substituters = https://cache.nixos.org https://nix-postgres-artifacts.s3.amazonaws.com" \
--extra-conf "trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI=% cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
ENV PATH="${PATH}:/nix/var/nix/profiles/default/bin"

View File

@ -0,0 +1,3 @@
/hack/
/vendor/
/www/

41
auth_v2.169.0/.gitattributes vendored Normal file
View File

@ -0,0 +1,41 @@
# Set the default behavior
* text=auto
# Go files
*.mod text eol=lf
*.sum text eol=lf
*.go text eol=lf
# Serialization
*.yml eol=lf
*.yaml eol=lf
*.toml eol=lf
*.json eol=lf
# Scripts
*.sh eol=lf
# DB files
*.sql eol=lf
# Html
*.html eol=lf
# Text and markdown files
*.txt text eol=lf
*.md text eol=lf
# Environment files/examples
*.env text eol=lf
# Docker files
.dockerignore text eol=lf
Dockerfile* text eol=lf
# Makefile
Makefile text eol=lf
# Git files
.gitignore text eol=lf
.gitattributes text eol=lf
.gitkeep text eol=lf

18
auth_v2.169.0/.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
.env*
vendor/
gotrue
gotrue-arm64
gotrue.exe
auth
auth-arm64
auth.exe
coverage.out
.DS_Store
.vscode
www/dist/
www/.DS_Store
www/node_modules
npm-debug.log
.data

10
auth_v2.169.0/.releaserc Normal file
View File

@ -0,0 +1,10 @@
{
"branches": [
"master"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/github"
]
}

601
auth_v2.169.0/CHANGELOG.md Normal file
View File

@ -0,0 +1,601 @@
# Changelog
## [2.169.0](https://github.com/supabase/auth/compare/v2.168.0...v2.169.0) (2025-01-27)
### Features
* add an optional burstable rate limiter ([#1924](https://github.com/supabase/auth/issues/1924)) ([1f06f58](https://github.com/supabase/auth/commit/1f06f58e1434b91612c0d96c8c0435d26570f3e2))
* cover 100% of crypto with tests ([#1892](https://github.com/supabase/auth/issues/1892)) ([174198e](https://github.com/supabase/auth/commit/174198e56f8e9b8470a717d0021c626130288d2e))
### Bug Fixes
* convert refreshed_at to UTC before updating ([#1916](https://github.com/supabase/auth/issues/1916)) ([a4c692f](https://github.com/supabase/auth/commit/a4c692f6cb1b8bf4c47ea012872af5ce93382fbf))
* correct casing of API key authentication in openapi.yaml ([0cfd177](https://github.com/supabase/auth/commit/0cfd177b8fb1df8f62e84fbd3761ef9f90c384de))
* improve invalid channel error message returned ([#1908](https://github.com/supabase/auth/issues/1908)) ([f72f0ee](https://github.com/supabase/auth/commit/f72f0eee328fa0aa041155f5f5dc305f0874d2bf))
* improve saml assertion logging ([#1915](https://github.com/supabase/auth/issues/1915)) ([d6030cc](https://github.com/supabase/auth/commit/d6030ccd271a381e2a6ababa11a5beae4b79e5c3))
## [2.168.0](https://github.com/supabase/auth/compare/v2.167.0...v2.168.0) (2025-01-06)
### Features
* set `email_verified` to true on all identities with the verified email ([#1902](https://github.com/supabase/auth/issues/1902)) ([307892f](https://github.com/supabase/auth/commit/307892f85b39150074fbb80b9c8f45ac3312aae2))
## [2.167.0](https://github.com/supabase/auth/compare/v2.166.0...v2.167.0) (2024-12-24)
### Features
* fix argon2 parsing and comparison ([#1887](https://github.com/supabase/auth/issues/1887)) ([9dbe6ef](https://github.com/supabase/auth/commit/9dbe6ef931ae94e621d55a5f7aea4b7ee0449949))
## [2.166.0](https://github.com/supabase/auth/compare/v2.165.0...v2.166.0) (2024-12-23)
### Features
* switch to googleapis/release-please-action, bump to 2.166.0 ([#1883](https://github.com/supabase/auth/issues/1883)) ([11a312f](https://github.com/supabase/auth/commit/11a312fcf77771b3732f2f439078225895df7a85))
### Bug Fixes
* check if session is nil ([#1873](https://github.com/supabase/auth/issues/1873)) ([fd82601](https://github.com/supabase/auth/commit/fd82601917adcd9f8c38263953eb1ef098b26b7f))
* email_verified field not being updated on signup confirmation ([#1868](https://github.com/supabase/auth/issues/1868)) ([483463e](https://github.com/supabase/auth/commit/483463e49eec7b2974cca05eadca6b933b2145b5))
* handle user banned error code ([#1851](https://github.com/supabase/auth/issues/1851)) ([a6918f4](https://github.com/supabase/auth/commit/a6918f49baee42899b3ae1b7b6bc126d84629c99))
* Revert "fix: revert fallback on btree indexes when hash is unavailable" ([#1859](https://github.com/supabase/auth/issues/1859)) ([9fe5b1e](https://github.com/supabase/auth/commit/9fe5b1eebfafb385d6b5d10196aeb2a1964ab296))
* skip cleanup for non-2xx status ([#1877](https://github.com/supabase/auth/issues/1877)) ([f572ced](https://github.com/supabase/auth/commit/f572ced3699c7f920deccce1a3539299541ec94c))
## [2.165.1](https://github.com/supabase/auth/compare/v2.165.0...v2.165.1) (2024-12-06)
### Bug Fixes
* allow setting the mailer service headers as strings ([#1861](https://github.com/supabase/auth/issues/1861)) ([7907b56](https://github.com/supabase/auth/commit/7907b566228f7e2d76049b44cfe0cc808c109100))
## [2.165.0](https://github.com/supabase/auth/compare/v2.164.0...v2.165.0) (2024-12-05)
### Features
* add email validation function to lower bounce rates ([#1845](https://github.com/supabase/auth/issues/1845)) ([2c291f0](https://github.com/supabase/auth/commit/2c291f0356f3e91063b6b43bf2a21625b0ce0ebd))
* use embedded migrations for `migrate` command ([#1843](https://github.com/supabase/auth/issues/1843)) ([e358da5](https://github.com/supabase/auth/commit/e358da5f0e267725a77308461d0a4126436fc537))
### Bug Fixes
* fallback on btree indexes when hash is unavailable ([#1856](https://github.com/supabase/auth/issues/1856)) ([b33bc31](https://github.com/supabase/auth/commit/b33bc31c07549dc9dc221100995d6f6b6754fd3a))
* return the error code instead of status code ([#1855](https://github.com/supabase/auth/issues/1855)) ([834a380](https://github.com/supabase/auth/commit/834a380d803ae9ce59ce5ee233fa3a78a984fe68))
* revert fallback on btree indexes when hash is unavailable ([#1858](https://github.com/supabase/auth/issues/1858)) ([1c7202f](https://github.com/supabase/auth/commit/1c7202ff835856562ee66b33be131eca769acf1d))
* update ip mismatch error message ([#1849](https://github.com/supabase/auth/issues/1849)) ([49fbbf0](https://github.com/supabase/auth/commit/49fbbf03917a1085c58e9a1ff76c247ae6bb9ca7))
## [2.164.0](https://github.com/supabase/auth/compare/v2.163.2...v2.164.0) (2024-11-13)
### Features
* return validation failed error if captcha request was not json ([#1815](https://github.com/supabase/auth/issues/1815)) ([26d2e36](https://github.com/supabase/auth/commit/26d2e36bba29eb8a6ddba556acfd0820f3bfde5d))
### Bug Fixes
* add error codes to refresh token flow ([#1824](https://github.com/supabase/auth/issues/1824)) ([4614dc5](https://github.com/supabase/auth/commit/4614dc54ab1dcb5390cfed05441e7888af017d92))
* add test coverage for rate limits with 0 permitted events ([#1834](https://github.com/supabase/auth/issues/1834)) ([7c3cf26](https://github.com/supabase/auth/commit/7c3cf26cfe2a3e4de579d10509945186ad719855))
* correct web authn aaguid column naming ([#1826](https://github.com/supabase/auth/issues/1826)) ([0a589d0](https://github.com/supabase/auth/commit/0a589d04e1cd9310cb260d329bc8beb050adf8da))
* default to files:read scope for Figma provider ([#1831](https://github.com/supabase/auth/issues/1831)) ([9ce2857](https://github.com/supabase/auth/commit/9ce28570bf3da9571198d44d693c7ad7038cde33))
* improve error messaging for http hooks ([#1821](https://github.com/supabase/auth/issues/1821)) ([fa020d0](https://github.com/supabase/auth/commit/fa020d0fc292d5c381c57ecac6666d9ff657e4c4))
* make drop_uniqueness_constraint_on_phone idempotent ([#1817](https://github.com/supabase/auth/issues/1817)) ([158e473](https://github.com/supabase/auth/commit/158e4732afa17620cdd89c85b7b57569feea5c21))
* possible panic if refresh token has a null session_id ([#1822](https://github.com/supabase/auth/issues/1822)) ([a7129df](https://github.com/supabase/auth/commit/a7129df4e1d91a042b56ff1f041b9c6598825475))
* rate limits of 0 take precedence over MAILER_AUTO_CONFIRM ([#1837](https://github.com/supabase/auth/issues/1837)) ([cb7894e](https://github.com/supabase/auth/commit/cb7894e1119d27d527dedcca22d8b3d433beddac))
## [2.163.2](https://github.com/supabase/auth/compare/v2.163.1...v2.163.2) (2024-10-22)
### Bug Fixes
* ignore rate limits for autoconfirm ([#1810](https://github.com/supabase/auth/issues/1810)) ([9ce2340](https://github.com/supabase/auth/commit/9ce23409f960a8efa55075931138624cb681eca5))
## [2.163.1](https://github.com/supabase/auth/compare/v2.163.0...v2.163.1) (2024-10-22)
### Bug Fixes
* external host validation ([#1808](https://github.com/supabase/auth/issues/1808)) ([4f6a461](https://github.com/supabase/auth/commit/4f6a4617074e61ba3b31836ccb112014904ce97c)), closes [#1228](https://github.com/supabase/auth/issues/1228)
## [2.163.0](https://github.com/supabase/auth/compare/v2.162.2...v2.163.0) (2024-10-15)
### Features
* add mail header support via `GOTRUE_SMTP_HEADERS` with `$messageType` ([#1804](https://github.com/supabase/auth/issues/1804)) ([99d6a13](https://github.com/supabase/auth/commit/99d6a134c44554a8ad06695e1dff54c942c8335d))
* add MFA for WebAuthn ([#1775](https://github.com/supabase/auth/issues/1775)) ([8cc2f0e](https://github.com/supabase/auth/commit/8cc2f0e14d06d0feb56b25a0278fda9e213b6b5a))
* configurable email and sms rate limiting ([#1800](https://github.com/supabase/auth/issues/1800)) ([5e94047](https://github.com/supabase/auth/commit/5e9404717e1c962ab729cde150ef5b40ea31a6e8))
* mailer logging ([#1805](https://github.com/supabase/auth/issues/1805)) ([9354b83](https://github.com/supabase/auth/commit/9354b83a48a3edcb49197c997a1e96efc80c5383))
* preserve rate limiters in memory across configuration reloads ([#1792](https://github.com/supabase/auth/issues/1792)) ([0a3968b](https://github.com/supabase/auth/commit/0a3968b02b9f044bfb7e5ebc71dca970d2bb7807))
### Bug Fixes
* add twilio verify support on mfa ([#1714](https://github.com/supabase/auth/issues/1714)) ([aeb5d8f](https://github.com/supabase/auth/commit/aeb5d8f8f18af60ce369cab5714979ac0c208308))
* email header setting no longer misleading ([#1802](https://github.com/supabase/auth/issues/1802)) ([3af03be](https://github.com/supabase/auth/commit/3af03be6b65c40f3f4f62ce9ab989a20d75ae53a))
* enforce authorized address checks on send email only ([#1806](https://github.com/supabase/auth/issues/1806)) ([c0c5b23](https://github.com/supabase/auth/commit/c0c5b23728c8fb633dae23aa4b29ed60e2691a2b))
* fix `getExcludedColumns` slice allocation ([#1788](https://github.com/supabase/auth/issues/1788)) ([7f006b6](https://github.com/supabase/auth/commit/7f006b63c8d7e28e55a6d471881e9c118df80585))
* Fix reqPath for bypass check for verify EP ([#1789](https://github.com/supabase/auth/issues/1789)) ([646dc66](https://github.com/supabase/auth/commit/646dc66ea8d59a7f78bf5a5e55d9b5065a718c23))
* inline mailme package for easy development ([#1803](https://github.com/supabase/auth/issues/1803)) ([fa6f729](https://github.com/supabase/auth/commit/fa6f729a027eff551db104550fa626088e00bc15))
## [2.162.2](https://github.com/supabase/auth/compare/v2.162.1...v2.162.2) (2024-10-05)
### Bug Fixes
* refactor mfa validation into functions ([#1780](https://github.com/supabase/auth/issues/1780)) ([410b8ac](https://github.com/supabase/auth/commit/410b8acdd659fc4c929fe57a9e9dba4c76da305d))
* upgrade ci Go version ([#1782](https://github.com/supabase/auth/issues/1782)) ([97a48f6](https://github.com/supabase/auth/commit/97a48f6daaa2edda5b568939cbb1007ccdf33cfc))
* validateEmail should normalise emails ([#1790](https://github.com/supabase/auth/issues/1790)) ([2e9b144](https://github.com/supabase/auth/commit/2e9b144a0cbf2d26d3c4c2eafbff1899a36aeb3b))
## [2.162.1](https://github.com/supabase/auth/compare/v2.162.0...v2.162.1) (2024-10-03)
### Bug Fixes
* bypass check for token & verify endpoints ([#1785](https://github.com/supabase/auth/issues/1785)) ([9ac2ea0](https://github.com/supabase/auth/commit/9ac2ea0180826cd2f65e679524aabfb10666e973))
## [2.162.0](https://github.com/supabase/auth/compare/v2.161.0...v2.162.0) (2024-09-27)
### Features
* add support for migration of firebase scrypt passwords ([#1768](https://github.com/supabase/auth/issues/1768)) ([ba00f75](https://github.com/supabase/auth/commit/ba00f75c28d6708ddf8ee151ce18f2d6193689ef))
### Bug Fixes
* apply authorized email restriction to non-admin routes ([#1778](https://github.com/supabase/auth/issues/1778)) ([1af203f](https://github.com/supabase/auth/commit/1af203f92372e6db12454a0d319aad8ce3d149e7))
* magiclink failing due to passwordStrength check ([#1769](https://github.com/supabase/auth/issues/1769)) ([7a5411f](https://github.com/supabase/auth/commit/7a5411f1d4247478f91027bc4969cbbe95b7774c))
## [2.161.0](https://github.com/supabase/auth/compare/v2.160.0...v2.161.0) (2024-09-24)
### Features
* add `x-sb-error-code` header, show error code in logs ([#1765](https://github.com/supabase/auth/issues/1765)) ([ed91c59](https://github.com/supabase/auth/commit/ed91c59aa332738bd0ac4b994aeec2cdf193a068))
* add webauthn configuration variables ([#1773](https://github.com/supabase/auth/issues/1773)) ([77d5897](https://github.com/supabase/auth/commit/77d58976ae624dbb7f8abee041dd4557aab81109))
* config reloading ([#1771](https://github.com/supabase/auth/issues/1771)) ([6ee0091](https://github.com/supabase/auth/commit/6ee009163bfe451e2a0b923705e073928a12c004))
### Bug Fixes
* add additional information around errors for missing content type header ([#1576](https://github.com/supabase/auth/issues/1576)) ([c2b2f96](https://github.com/supabase/auth/commit/c2b2f96f07c97c15597cd972b1cd672238d87cdc))
* add token to hook payload for non-secure email change ([#1763](https://github.com/supabase/auth/issues/1763)) ([7e472ad](https://github.com/supabase/auth/commit/7e472ad72042e86882dab3fddce9fafa66a8236c))
* update aal requirements to update user ([#1766](https://github.com/supabase/auth/issues/1766)) ([25d9874](https://github.com/supabase/auth/commit/25d98743f6cc2cca2b490a087f468c8556ec5e44))
* update mfa admin methods ([#1774](https://github.com/supabase/auth/issues/1774)) ([567ea7e](https://github.com/supabase/auth/commit/567ea7ebd18eacc5e6daea8adc72e59e94459991))
* user sanitization should clean up email change info too ([#1759](https://github.com/supabase/auth/issues/1759)) ([9d419b4](https://github.com/supabase/auth/commit/9d419b400f0637b10e5c235b8fd5bac0d69352bd))
## [2.160.0](https://github.com/supabase/auth/compare/v2.159.2...v2.160.0) (2024-09-02)
### Features
* add authorized email address support ([#1757](https://github.com/supabase/auth/issues/1757)) ([f3a28d1](https://github.com/supabase/auth/commit/f3a28d182d193cf528cc72a985dfeaf7ecb67056))
* add option to disable magic links ([#1756](https://github.com/supabase/auth/issues/1756)) ([2ad0737](https://github.com/supabase/auth/commit/2ad07373aa9239eba94abdabbb01c9abfa8c48de))
* add support for saml encrypted assertions ([#1752](https://github.com/supabase/auth/issues/1752)) ([c5480ef](https://github.com/supabase/auth/commit/c5480ef83248ec2e7e3d3d87f92f43f17161ed25))
### Bug Fixes
* apply shared limiters before email / sms is sent ([#1748](https://github.com/supabase/auth/issues/1748)) ([bf276ab](https://github.com/supabase/auth/commit/bf276ab49753642793471815727559172fea4efc))
* simplify WaitForCleanup ([#1747](https://github.com/supabase/auth/issues/1747)) ([0084625](https://github.com/supabase/auth/commit/0084625ad0790dd7c14b412d932425f4b84bb4c8))
## [2.159.2](https://github.com/supabase/auth/compare/v2.159.1...v2.159.2) (2024-08-28)
### Bug Fixes
* allow anonymous user to update password ([#1739](https://github.com/supabase/auth/issues/1739)) ([2d51956](https://github.com/supabase/auth/commit/2d519569d7b8540886d0a64bf3e561ef5f91eb63))
* hide hook name ([#1743](https://github.com/supabase/auth/issues/1743)) ([7e38f4c](https://github.com/supabase/auth/commit/7e38f4cf37768fe2adf92bbd0723d1d521b3d74c))
* remove server side cookie token methods ([#1742](https://github.com/supabase/auth/issues/1742)) ([c6efec4](https://github.com/supabase/auth/commit/c6efec4cbc950e01e1fd06d45ed821bd27c2ad08))
## [2.159.1](https://github.com/supabase/auth/compare/v2.159.0...v2.159.1) (2024-08-23)
### Bug Fixes
* return oauth identity when user is created ([#1736](https://github.com/supabase/auth/issues/1736)) ([60cfb60](https://github.com/supabase/auth/commit/60cfb6063afa574dfe4993df6b0e087d4df71309))
## [2.159.0](https://github.com/supabase/auth/compare/v2.158.1...v2.159.0) (2024-08-21)
### Features
* Vercel marketplace OIDC ([#1731](https://github.com/supabase/auth/issues/1731)) ([a9ff361](https://github.com/supabase/auth/commit/a9ff3612196af4a228b53a8bfb9c11785bcfba8d))
### Bug Fixes
* add error codes to password login flow ([#1721](https://github.com/supabase/auth/issues/1721)) ([4351226](https://github.com/supabase/auth/commit/435122627a0784f1c5cb76d7e08caa1f6259423b))
* change phone constraint to per user ([#1713](https://github.com/supabase/auth/issues/1713)) ([b9bc769](https://github.com/supabase/auth/commit/b9bc769b93b6e700925fcbc1ebf8bf9678034205))
* custom SMS does not work with Twilio Verify ([#1733](https://github.com/supabase/auth/issues/1733)) ([dc2391d](https://github.com/supabase/auth/commit/dc2391d15f2c0725710aa388cd32a18797e6769c))
* ignore errors if transaction has closed already ([#1726](https://github.com/supabase/auth/issues/1726)) ([53c11d1](https://github.com/supabase/auth/commit/53c11d173a79ae5c004871b1b5840c6f9425a080))
* redirect invalid state errors to site url ([#1722](https://github.com/supabase/auth/issues/1722)) ([b2b1123](https://github.com/supabase/auth/commit/b2b11239dc9f9bd3c85d76f6c23ee94beb3330bb))
* remove TOTP field for phone enroll response ([#1717](https://github.com/supabase/auth/issues/1717)) ([4b04327](https://github.com/supabase/auth/commit/4b043275dd2d94600a8138d4ebf4638754ed926b))
* use signing jwk to sign oauth state ([#1728](https://github.com/supabase/auth/issues/1728)) ([66fd0c8](https://github.com/supabase/auth/commit/66fd0c8434388bbff1e1bf02f40517aca0e9d339))
## [2.158.1](https://github.com/supabase/auth/compare/v2.158.0...v2.158.1) (2024-08-05)
### Bug Fixes
* add last_challenged_at field to mfa factors ([#1705](https://github.com/supabase/auth/issues/1705)) ([29cbeb7](https://github.com/supabase/auth/commit/29cbeb799ff35ce528bfbd01b7103a24903d8061))
* allow enabling sms hook without setting up sms provider ([#1704](https://github.com/supabase/auth/issues/1704)) ([575e88a](https://github.com/supabase/auth/commit/575e88ac345adaeb76ab6aae077307fdab9cda3c))
* drop the MFA_ENABLED config ([#1701](https://github.com/supabase/auth/issues/1701)) ([078c3a8](https://github.com/supabase/auth/commit/078c3a8adcd51e57b68ab1b582549f5813cccd14))
* enforce uniqueness on verified phone numbers ([#1693](https://github.com/supabase/auth/issues/1693)) ([70446cc](https://github.com/supabase/auth/commit/70446cc11d70b0493d742fe03f272330bb5b633e))
* expose `X-Supabase-Api-Version` header in CORS ([#1612](https://github.com/supabase/auth/issues/1612)) ([6ccd814](https://github.com/supabase/auth/commit/6ccd814309dca70a9e3585543887194b05d725d3))
* include factor_id in query ([#1702](https://github.com/supabase/auth/issues/1702)) ([ac14e82](https://github.com/supabase/auth/commit/ac14e82b33545466184da99e99b9d3fe5f3876d9))
* move is owned by check to load factor ([#1703](https://github.com/supabase/auth/issues/1703)) ([701a779](https://github.com/supabase/auth/commit/701a779cf092e777dd4ad4954dc650164b09ab32))
* refactor TOTP MFA into separate methods ([#1698](https://github.com/supabase/auth/issues/1698)) ([250d92f](https://github.com/supabase/auth/commit/250d92f9a18d38089d1bf262ef9088022a446965))
* remove check for content-length ([#1700](https://github.com/supabase/auth/issues/1700)) ([81b332d](https://github.com/supabase/auth/commit/81b332d2f48622008469d2c5a9b130465a65f2a3))
* remove FindFactorsByUser ([#1707](https://github.com/supabase/auth/issues/1707)) ([af8e2dd](https://github.com/supabase/auth/commit/af8e2dda15a1234a05e7d2d34d316eaa029e0912))
* update openapi spec for MFA (Phone) ([#1689](https://github.com/supabase/auth/issues/1689)) ([a3da4b8](https://github.com/supabase/auth/commit/a3da4b89820c37f03ea128889616aca598d99f68))
## [2.158.0](https://github.com/supabase/auth/compare/v2.157.0...v2.158.0) (2024-07-31)
### Features
* add hook log entry with `run_hook` action ([#1684](https://github.com/supabase/auth/issues/1684)) ([46491b8](https://github.com/supabase/auth/commit/46491b867a4f5896494417391392a373a453fa5f))
* MFA (Phone) ([#1668](https://github.com/supabase/auth/issues/1668)) ([ae091aa](https://github.com/supabase/auth/commit/ae091aa942bdc5bc97481037508ec3bb4079d859))
### Bug Fixes
* maintain backward compatibility for asymmetric JWTs ([#1690](https://github.com/supabase/auth/issues/1690)) ([0ad1402](https://github.com/supabase/auth/commit/0ad1402444348e47e1e42be186b3f052d31be824))
* MFA NewFactor to default to creating unverfied factors ([#1692](https://github.com/supabase/auth/issues/1692)) ([3d448fa](https://github.com/supabase/auth/commit/3d448fa73cb77eb8511dbc47bfafecce4a4a2150))
* minor spelling errors ([#1688](https://github.com/supabase/auth/issues/1688)) ([6aca52b](https://github.com/supabase/auth/commit/6aca52b56f8a6254de7709c767b9a5649f1da248)), closes [#1682](https://github.com/supabase/auth/issues/1682)
* treat `GOTRUE_MFA_ENABLED` as meaning TOTP enabled on enroll and verify ([#1694](https://github.com/supabase/auth/issues/1694)) ([8015251](https://github.com/supabase/auth/commit/8015251400bd52cbdad3ea28afb83b1cdfe816dd))
* update mfa phone migration to be idempotent ([#1687](https://github.com/supabase/auth/issues/1687)) ([fdff1e7](https://github.com/supabase/auth/commit/fdff1e703bccf93217636266f1862bd0a9205edb))
## [2.157.0](https://github.com/supabase/auth/compare/v2.156.0...v2.157.0) (2024-07-26)
### Features
* add asymmetric jwt support ([#1674](https://github.com/supabase/auth/issues/1674)) ([c7a2be3](https://github.com/supabase/auth/commit/c7a2be347b301b666e99adc3d3fed78c5e287c82))
## [2.156.0](https://github.com/supabase/auth/compare/v2.155.6...v2.156.0) (2024-07-25)
### Features
* add is_anonymous claim to Auth hook jsonschema ([#1667](https://github.com/supabase/auth/issues/1667)) ([f9df65c](https://github.com/supabase/auth/commit/f9df65c91e226084abfa2e868ab6bab892d16d2f))
### Bug Fixes
* restrict autoconfirm email change to anonymous users ([#1679](https://github.com/supabase/auth/issues/1679)) ([b57e223](https://github.com/supabase/auth/commit/b57e2230102280ed873acf70be1aeb5a2f6f7a4f))
## [2.155.6](https://github.com/supabase/auth/compare/v2.155.5...v2.155.6) (2024-07-22)
### Bug Fixes
* use deep equal ([#1672](https://github.com/supabase/auth/issues/1672)) ([8efd57d](https://github.com/supabase/auth/commit/8efd57dab40346762a04bac61b314ce05d6fa69c))
## [2.155.5](https://github.com/supabase/auth/compare/v2.155.4...v2.155.5) (2024-07-19)
### Bug Fixes
* check password max length in checkPasswordStrength ([#1659](https://github.com/supabase/auth/issues/1659)) ([1858c93](https://github.com/supabase/auth/commit/1858c93bba6f5bc41e4c65489f12c1a0786a1f2b))
* don't update attribute mapping if nil ([#1665](https://github.com/supabase/auth/issues/1665)) ([7e67f3e](https://github.com/supabase/auth/commit/7e67f3edbf81766df297a66f52a8e472583438c6))
* refactor mfa models and add observability to loadFactor ([#1669](https://github.com/supabase/auth/issues/1669)) ([822fb93](https://github.com/supabase/auth/commit/822fb93faab325ba3d4bb628dff43381d68d0b5d))
## [2.155.4](https://github.com/supabase/auth/compare/v2.155.3...v2.155.4) (2024-07-17)
### Bug Fixes
* treat empty string as nil in `encrypted_password` ([#1663](https://github.com/supabase/auth/issues/1663)) ([f99286e](https://github.com/supabase/auth/commit/f99286eaed505daf3db6f381265ef6024e7e36d2))
## [2.155.3](https://github.com/supabase/auth/compare/v2.155.2...v2.155.3) (2024-07-12)
### Bug Fixes
* serialize jwt as string ([#1657](https://github.com/supabase/auth/issues/1657)) ([98d8324](https://github.com/supabase/auth/commit/98d83245e40d606438eb0afdbf474276179fd91d))
## [2.155.2](https://github.com/supabase/auth/compare/v2.155.1...v2.155.2) (2024-07-12)
### Bug Fixes
* improve session error logging ([#1655](https://github.com/supabase/auth/issues/1655)) ([5a6793e](https://github.com/supabase/auth/commit/5a6793ee8fce7a089750fe10b3b63bb0a19d6d21))
* omit empty string from name & use case-insensitive equality for comparing SAML attributes ([#1654](https://github.com/supabase/auth/issues/1654)) ([bf5381a](https://github.com/supabase/auth/commit/bf5381a6b1c686955dc4e39fe5fb806ffd309563))
* set rate limit log level to warn ([#1652](https://github.com/supabase/auth/issues/1652)) ([10ca9c8](https://github.com/supabase/auth/commit/10ca9c806e4b67a371897f1b3f93c515764c4240))
## [2.155.1](https://github.com/supabase/auth/compare/v2.155.0...v2.155.1) (2024-07-04)
### Bug Fixes
* apply mailer autoconfirm config to update user email ([#1646](https://github.com/supabase/auth/issues/1646)) ([a518505](https://github.com/supabase/auth/commit/a5185058e72509b0781e0eb59910ecdbb8676fee))
* check for empty aud string ([#1649](https://github.com/supabase/auth/issues/1649)) ([42c1d45](https://github.com/supabase/auth/commit/42c1d4526b98203664d4a22c23014ecd0b4951f9))
* return proper error if sms rate limit is exceeded ([#1647](https://github.com/supabase/auth/issues/1647)) ([3c8d765](https://github.com/supabase/auth/commit/3c8d7656431ac4b2e80726b7c37adb8f0c778495))
## [2.155.0](https://github.com/supabase/auth/compare/v2.154.2...v2.155.0) (2024-07-03)
### Features
* add `password_hash` and `id` fields to admin create user ([#1641](https://github.com/supabase/auth/issues/1641)) ([20d59f1](https://github.com/supabase/auth/commit/20d59f10b601577683d05bcd7d2128ff4bc462a0))
### Bug Fixes
* improve mfa verify logs ([#1635](https://github.com/supabase/auth/issues/1635)) ([d8b47f9](https://github.com/supabase/auth/commit/d8b47f9d3f0dc8f97ad1de49e45f452ebc726481))
* invited users should have a temporary password generated ([#1644](https://github.com/supabase/auth/issues/1644)) ([3f70d9d](https://github.com/supabase/auth/commit/3f70d9d8974d0e9c437c51e1312ad17ce9056ec9))
* upgrade golang-jwt to v5 ([#1639](https://github.com/supabase/auth/issues/1639)) ([2cb97f0](https://github.com/supabase/auth/commit/2cb97f080fa4695766985cc4792d09476534be68))
* use pointer for `user.EncryptedPassword` ([#1637](https://github.com/supabase/auth/issues/1637)) ([bbecbd6](https://github.com/supabase/auth/commit/bbecbd61a46b0c528b1191f48d51f166c06f4b16))
## [2.154.2](https://github.com/supabase/auth/compare/v2.154.1...v2.154.2) (2024-06-24)
### Bug Fixes
* publish to ghcr.io/supabase/auth ([#1626](https://github.com/supabase/auth/issues/1626)) ([930aa3e](https://github.com/supabase/auth/commit/930aa3edb633823d4510c2aff675672df06f1211)), closes [#1625](https://github.com/supabase/auth/issues/1625)
* revert define search path in auth functions ([#1634](https://github.com/supabase/auth/issues/1634)) ([155e87e](https://github.com/supabase/auth/commit/155e87ef8129366d665968f64d1fc66676d07e16))
* update MaxFrequency error message to reflect number of seconds ([#1540](https://github.com/supabase/auth/issues/1540)) ([e81c25d](https://github.com/supabase/auth/commit/e81c25d19551fdebfc5197d96bc220ddb0f8227b))
## [2.154.1](https://github.com/supabase/auth/compare/v2.154.0...v2.154.1) (2024-06-17)
### Bug Fixes
* add ip based limiter ([#1622](https://github.com/supabase/auth/issues/1622)) ([06464c0](https://github.com/supabase/auth/commit/06464c013571253d1f18f7ae5e840826c4bd84a7))
* admin user update should update is_anonymous field ([#1623](https://github.com/supabase/auth/issues/1623)) ([f5c6fcd](https://github.com/supabase/auth/commit/f5c6fcd9c3fee0f793f96880a8caebc5b5cb0916))
## [2.154.0](https://github.com/supabase/auth/compare/v2.153.0...v2.154.0) (2024-06-12)
### Features
* add max length check for email ([#1508](https://github.com/supabase/auth/issues/1508)) ([f9c13c0](https://github.com/supabase/auth/commit/f9c13c0ad5c556bede49d3e0f6e5f58ca26161c3))
* add support for Slack OAuth V2 ([#1591](https://github.com/supabase/auth/issues/1591)) ([bb99251](https://github.com/supabase/auth/commit/bb992519cdf7578dc02cd7de55e2e6aa09b4c0f3))
* encrypt sensitive columns ([#1593](https://github.com/supabase/auth/issues/1593)) ([e4a4758](https://github.com/supabase/auth/commit/e4a475820b2dc1f985bd37df15a8ab9e781626f5))
* upgrade otel to v1.26 ([#1585](https://github.com/supabase/auth/issues/1585)) ([cdd13ad](https://github.com/supabase/auth/commit/cdd13adec02eb0c9401bc55a2915c1005d50dea1))
* use largest avatar from spotify instead ([#1210](https://github.com/supabase/auth/issues/1210)) ([4f9994b](https://github.com/supabase/auth/commit/4f9994bf792c3887f2f45910b11a9c19ee3a896b)), closes [#1209](https://github.com/supabase/auth/issues/1209)
### Bug Fixes
* define search path in auth functions ([#1616](https://github.com/supabase/auth/issues/1616)) ([357bda2](https://github.com/supabase/auth/commit/357bda23cb2abd12748df80a9d27288aa548534d))
* enable rls & update grants for auth tables ([#1617](https://github.com/supabase/auth/issues/1617)) ([28967aa](https://github.com/supabase/auth/commit/28967aa4b5db2363cc581c9da0d64e974eb7b64c))
## [2.153.0](https://github.com/supabase/auth/compare/v2.152.0...v2.153.0) (2024-06-04)
### Features
* add SAML specific external URL config ([#1599](https://github.com/supabase/auth/issues/1599)) ([b352719](https://github.com/supabase/auth/commit/b3527190560381fafe9ba2fae4adc3b73703024a))
* add support for verifying argon2i and argon2id passwords ([#1597](https://github.com/supabase/auth/issues/1597)) ([55409f7](https://github.com/supabase/auth/commit/55409f797bea55068a3fafdddd6cfdb78feba1b4))
* make the email client explicity set the format to be HTML ([#1149](https://github.com/supabase/auth/issues/1149)) ([53e223a](https://github.com/supabase/auth/commit/53e223abdf29f4abcad13f99baf00daedcb00c3f))
### Bug Fixes
* call write header in write if not written ([#1598](https://github.com/supabase/auth/issues/1598)) ([0ef7eb3](https://github.com/supabase/auth/commit/0ef7eb30619d4c365e06a94a79b9cb0333d792da))
* deadlock issue with timeout middleware write ([#1595](https://github.com/supabase/auth/issues/1595)) ([6c9fbd4](https://github.com/supabase/auth/commit/6c9fbd4bd5623c729906fca7857ab508166a3056))
* improve token OIDC logging ([#1606](https://github.com/supabase/auth/issues/1606)) ([5262683](https://github.com/supabase/auth/commit/526268311844467664e89c8329e5aaee817dbbaf))
* update contributing to use v1.22 ([#1609](https://github.com/supabase/auth/issues/1609)) ([5894d9e](https://github.com/supabase/auth/commit/5894d9e41e7681512a9904ad47082a705e948c98))
## [2.152.0](https://github.com/supabase/auth/compare/v2.151.0...v2.152.0) (2024-05-22)
### Features
* new timeout writer implementation ([#1584](https://github.com/supabase/auth/issues/1584)) ([72614a1](https://github.com/supabase/auth/commit/72614a1fce27888f294772b512f8e31c55a36d87))
* remove legacy lookup in users for one_time_tokens (phase II) ([#1569](https://github.com/supabase/auth/issues/1569)) ([39ca026](https://github.com/supabase/auth/commit/39ca026035f6c61d206d31772c661b326c2a424c))
* update chi version ([#1581](https://github.com/supabase/auth/issues/1581)) ([c64ae3d](https://github.com/supabase/auth/commit/c64ae3dd775e8fb3022239252c31b4ee73893237))
* update openapi spec with identity and is_anonymous fields ([#1573](https://github.com/supabase/auth/issues/1573)) ([86a79df](https://github.com/supabase/auth/commit/86a79df9ecfcf09fda0b8e07afbc41154fbb7d9d))
### Bug Fixes
* improve logging structure ([#1583](https://github.com/supabase/auth/issues/1583)) ([c22fc15](https://github.com/supabase/auth/commit/c22fc15d2a8383e95a2364f383dfa7dce5f5df88))
* sms verify should update is_anonymous field ([#1580](https://github.com/supabase/auth/issues/1580)) ([e5f98cb](https://github.com/supabase/auth/commit/e5f98cb9e24ecebb0b7dc88c495fd456cc73fcba))
* use api_external_url domain as localname ([#1575](https://github.com/supabase/auth/issues/1575)) ([ed2b490](https://github.com/supabase/auth/commit/ed2b4907244281e4c54aaef74b1f4c8a8e3d97c9))
## [2.151.0](https://github.com/supabase/auth/compare/v2.150.1...v2.151.0) (2024-05-06)
### Features
* refactor one-time tokens for performance ([#1558](https://github.com/supabase/auth/issues/1558)) ([d1cf8d9](https://github.com/supabase/auth/commit/d1cf8d9096e9183d7772b73031de8ecbd66e912b))
### Bug Fixes
* do call send sms hook when SMS autoconfirm is enabled ([#1562](https://github.com/supabase/auth/issues/1562)) ([bfe4d98](https://github.com/supabase/auth/commit/bfe4d988f3768b0407526bcc7979fb21d8cbebb3))
* format test otps ([#1567](https://github.com/supabase/auth/issues/1567)) ([434a59a](https://github.com/supabase/auth/commit/434a59ae387c35fd6629ec7c674d439537e344e5))
* log final writer error instead of handling ([#1564](https://github.com/supabase/auth/issues/1564)) ([170bd66](https://github.com/supabase/auth/commit/170bd6615405afc852c7107f7358dfc837bad737))
## [2.150.1](https://github.com/supabase/auth/compare/v2.150.0...v2.150.1) (2024-04-28)
### Bug Fixes
* add db conn max idle time setting ([#1555](https://github.com/supabase/auth/issues/1555)) ([2caa7b4](https://github.com/supabase/auth/commit/2caa7b4d75d2ff54af20f3e7a30a8eeec8cbcda9))
## [2.150.0](https://github.com/supabase/auth/compare/v2.149.0...v2.150.0) (2024-04-25)
### Features
* add support for Azure CIAM login ([#1541](https://github.com/supabase/auth/issues/1541)) ([1cb4f96](https://github.com/supabase/auth/commit/1cb4f96bdc7ef3ef995781b4cf3c4364663a2bf3))
* add timeout middleware ([#1529](https://github.com/supabase/auth/issues/1529)) ([f96ff31](https://github.com/supabase/auth/commit/f96ff31040b28e3a7373b4fd41b7334eda1b413e))
* allow for postgres and http functions on each extensibility point ([#1528](https://github.com/supabase/auth/issues/1528)) ([348a1da](https://github.com/supabase/auth/commit/348a1daee24f6e44b14c018830b748e46d34b4c2))
* merge provider metadata on link account ([#1552](https://github.com/supabase/auth/issues/1552)) ([bd8b5c4](https://github.com/supabase/auth/commit/bd8b5c41dd544575e1a52ccf1ef3f0fdee67458c))
* send over user in SendSMS Hook instead of UserID ([#1551](https://github.com/supabase/auth/issues/1551)) ([d4d743c](https://github.com/supabase/auth/commit/d4d743c2ae9490e1b3249387e3b0d60df6913c68))
### Bug Fixes
* return error if session id does not exist ([#1538](https://github.com/supabase/auth/issues/1538)) ([91e9eca](https://github.com/supabase/auth/commit/91e9ecabe33a1c022f8e82a6050c22a7ca42de48))
## [2.149.0](https://github.com/supabase/auth/compare/v2.148.0...v2.149.0) (2024-04-15)
### Features
* refactor generate accesss token to take in request ([#1531](https://github.com/supabase/auth/issues/1531)) ([e4f2b59](https://github.com/supabase/auth/commit/e4f2b59e8e1f8158b6461a384349f1a32cc1bf9a))
### Bug Fixes
* linkedin_oidc provider error ([#1534](https://github.com/supabase/auth/issues/1534)) ([4f5e8e5](https://github.com/supabase/auth/commit/4f5e8e5120531e5a103fbdda91b51cabcb4e1a8c))
* revert patch for linkedin_oidc provider error ([#1535](https://github.com/supabase/auth/issues/1535)) ([58ef4af](https://github.com/supabase/auth/commit/58ef4af0b4224b78cd9e59428788d16a8d31e562))
* update linkedin issuer url ([#1536](https://github.com/supabase/auth/issues/1536)) ([10d6d8b](https://github.com/supabase/auth/commit/10d6d8b1eafa504da2b2a351d1f64a3a832ab1b9))
## [2.148.0](https://github.com/supabase/auth/compare/v2.147.1...v2.148.0) (2024-04-10)
### Features
* add array attribute mapping for SAML ([#1526](https://github.com/supabase/auth/issues/1526)) ([7326285](https://github.com/supabase/auth/commit/7326285c8af5c42e5c0c2d729ab224cf33ac3a1f))
## [2.147.1](https://github.com/supabase/auth/compare/v2.147.0...v2.147.1) (2024-04-09)
### Bug Fixes
* add validation and proper decoding on send email hook ([#1520](https://github.com/supabase/auth/issues/1520)) ([e19e762](https://github.com/supabase/auth/commit/e19e762e3e29729a1d1164c65461427822cc87f1))
* remove deprecated LogoutAllRefreshTokens ([#1519](https://github.com/supabase/auth/issues/1519)) ([35533ea](https://github.com/supabase/auth/commit/35533ea100669559e1209ecc7b091db3657234d9))
## [2.147.0](https://github.com/supabase/auth/compare/v2.146.0...v2.147.0) (2024-04-05)
### Features
* add send email Hook ([#1512](https://github.com/supabase/auth/issues/1512)) ([cf42e02](https://github.com/supabase/auth/commit/cf42e02ec63779f52b1652a7413f64994964c82d))
## [2.146.0](https://github.com/supabase/auth/compare/v2.145.0...v2.146.0) (2024-04-03)
### Features
* add custom sms hook ([#1474](https://github.com/supabase/auth/issues/1474)) ([0f6b29a](https://github.com/supabase/auth/commit/0f6b29a46f1dcbf92aa1f7cb702f42e7640f5f93))
* forbid generating an access token without a session ([#1504](https://github.com/supabase/auth/issues/1504)) ([795e93d](https://github.com/supabase/auth/commit/795e93d0afbe94bcd78489a3319a970b7bf8e8bc))
### Bug Fixes
* add cleanup statement for anonymous users ([#1497](https://github.com/supabase/auth/issues/1497)) ([cf2372a](https://github.com/supabase/auth/commit/cf2372a177796b829b72454e7491ce768bf5a42f))
* generate signup link should not error ([#1514](https://github.com/supabase/auth/issues/1514)) ([4fc3881](https://github.com/supabase/auth/commit/4fc388186ac7e7a9a32ca9b963a83d6ac2eb7603))
* move all EmailActionTypes to mailer package ([#1510](https://github.com/supabase/auth/issues/1510)) ([765db08](https://github.com/supabase/auth/commit/765db08582669a1b7f054217fa8f0ed45804c0b5))
* refactor mfa and aal update methods ([#1503](https://github.com/supabase/auth/issues/1503)) ([31a5854](https://github.com/supabase/auth/commit/31a585429bf248aa919d94c82c7c9e0c1c695461))
* rename from CustomSMSProvider to SendSMS ([#1513](https://github.com/supabase/auth/issues/1513)) ([c0bc37b](https://github.com/supabase/auth/commit/c0bc37b44effaebb62ba85102f072db07fe57e48))
## [2.145.0](https://github.com/supabase/gotrue/compare/v2.144.0...v2.145.0) (2024-03-26)
### Features
* add error codes ([#1377](https://github.com/supabase/gotrue/issues/1377)) ([e4beea1](https://github.com/supabase/gotrue/commit/e4beea1cdb80544b0581f1882696a698fdf64938))
* add kakao OIDC ([#1381](https://github.com/supabase/gotrue/issues/1381)) ([b5566e7](https://github.com/supabase/gotrue/commit/b5566e7ac001cc9f2bac128de0fcb908caf3a5ed))
* clean up expired factors ([#1371](https://github.com/supabase/gotrue/issues/1371)) ([5c94207](https://github.com/supabase/gotrue/commit/5c9420743a9aef0675f823c30aa4525b4933836e))
* configurable NameID format for SAML provider ([#1481](https://github.com/supabase/gotrue/issues/1481)) ([ef405d8](https://github.com/supabase/gotrue/commit/ef405d89e69e008640f275bc37f8ec02ad32da40))
* HTTP Hook - Add custom envconfig decoding for HTTP Hook Secrets ([#1467](https://github.com/supabase/gotrue/issues/1467)) ([5b24c4e](https://github.com/supabase/gotrue/commit/5b24c4eb05b2b52c4177d5f41cba30cb68495c8c))
* refactor PKCE FlowState to reduce duplicate code ([#1446](https://github.com/supabase/gotrue/issues/1446)) ([b8d0337](https://github.com/supabase/gotrue/commit/b8d0337922c6712380f6dc74f7eac9fb71b1ae48))
### Bug Fixes
* add http support for https hooks on localhost ([#1484](https://github.com/supabase/gotrue/issues/1484)) ([5c04104](https://github.com/supabase/gotrue/commit/5c04104bf77a9c2db46d009764ec3ec3e484fc09))
* cleanup panics due to bad inactivity timeout code ([#1471](https://github.com/supabase/gotrue/issues/1471)) ([548edf8](https://github.com/supabase/gotrue/commit/548edf898161c9ba9a136fc99ec2d52a8ba1f856))
* **docs:** remove bracket on file name for broken link ([#1493](https://github.com/supabase/gotrue/issues/1493)) ([96f7a68](https://github.com/supabase/gotrue/commit/96f7a68a5479825e31106c2f55f82d5b2c007c0f))
* impose expiry on auth code instead of magic link ([#1440](https://github.com/supabase/gotrue/issues/1440)) ([35aeaf1](https://github.com/supabase/gotrue/commit/35aeaf1b60dd27a22662a6d1955d60cc907b55dd))
* invalidate email, phone OTPs on password change ([#1489](https://github.com/supabase/gotrue/issues/1489)) ([960a4f9](https://github.com/supabase/gotrue/commit/960a4f94f5500e33a0ec2f6afe0380bbc9562500))
* move creation of flow state into function ([#1470](https://github.com/supabase/gotrue/issues/1470)) ([4392a08](https://github.com/supabase/gotrue/commit/4392a08d68d18828005d11382730117a7b143635))
* prevent user email side-channel leak on verify ([#1472](https://github.com/supabase/gotrue/issues/1472)) ([311cde8](https://github.com/supabase/gotrue/commit/311cde8d1e82f823ae26a341e068034d60273864))
* refactor email sending functions ([#1495](https://github.com/supabase/gotrue/issues/1495)) ([285c290](https://github.com/supabase/gotrue/commit/285c290adf231fea7ca1dff954491dc427cf18e2))
* refactor factor_test to centralize setup ([#1473](https://github.com/supabase/gotrue/issues/1473)) ([c86007e](https://github.com/supabase/gotrue/commit/c86007e59684334b5e8c2285c36094b6eec89442))
* refactor mfa challenge and tests ([#1469](https://github.com/supabase/gotrue/issues/1469)) ([6c76f21](https://github.com/supabase/gotrue/commit/6c76f21cee5dbef0562c37df6a546939affb2f8d))
* Resend SMS when duplicate SMS sign ups are made ([#1490](https://github.com/supabase/gotrue/issues/1490)) ([73240a0](https://github.com/supabase/gotrue/commit/73240a0b096977703e3c7d24a224b5641ce47c81))
* unlink identity bugs ([#1475](https://github.com/supabase/gotrue/issues/1475)) ([73e8d87](https://github.com/supabase/gotrue/commit/73e8d8742de3575b3165a707b5d2f486b2598d9d))
## [2.144.0](https://github.com/supabase/gotrue/compare/v2.143.0...v2.144.0) (2024-03-04)
### Features
* add configuration for custom sms sender hook ([#1428](https://github.com/supabase/gotrue/issues/1428)) ([1ea56b6](https://github.com/supabase/gotrue/commit/1ea56b62d47edb0766d9e445406ecb43d387d920))
* anonymous sign-ins ([#1460](https://github.com/supabase/gotrue/issues/1460)) ([130df16](https://github.com/supabase/gotrue/commit/130df165270c69c8e28aaa1b9421342f997c1ff3))
* clean up test setup in MFA tests ([#1452](https://github.com/supabase/gotrue/issues/1452)) ([7185af8](https://github.com/supabase/gotrue/commit/7185af8de4a269cdde2629054d222333d3522ebe))
* pass transaction to `invokeHook`, fixing pool exhaustion ([#1465](https://github.com/supabase/gotrue/issues/1465)) ([b536d36](https://github.com/supabase/gotrue/commit/b536d368f35adb31f937169e3f093d28352fa7be))
* refactor resource owner password grant ([#1443](https://github.com/supabase/gotrue/issues/1443)) ([e63ad6f](https://github.com/supabase/gotrue/commit/e63ad6ff0f67d9a83456918a972ecb5109125628))
* use dummy instance id to improve performance on refresh token queries ([#1454](https://github.com/supabase/gotrue/issues/1454)) ([656474e](https://github.com/supabase/gotrue/commit/656474e1b9ff3d5129190943e8c48e456625afe5))
### Bug Fixes
* expose `provider` under `amr` in access token ([#1456](https://github.com/supabase/gotrue/issues/1456)) ([e9f38e7](https://github.com/supabase/gotrue/commit/e9f38e76d8a7b93c5c2bb0de918a9b156155f018))
* improve MFA QR Code resilience so as to support providers like 1Password ([#1455](https://github.com/supabase/gotrue/issues/1455)) ([6522780](https://github.com/supabase/gotrue/commit/652278046c9dd92f5cecd778735b058ef3fb41c7))
* refactor request params to use generics ([#1464](https://github.com/supabase/gotrue/issues/1464)) ([e1cdf5c](https://github.com/supabase/gotrue/commit/e1cdf5c4b5c1bf467094f4bdcaa2e42a5cc51c20))
* revert refactor resource owner password grant ([#1466](https://github.com/supabase/gotrue/issues/1466)) ([fa21244](https://github.com/supabase/gotrue/commit/fa21244fa929709470c2e1fc4092a9ce947399e7))
* update file name so migration to Drop IP Address is applied ([#1447](https://github.com/supabase/gotrue/issues/1447)) ([f29e89d](https://github.com/supabase/gotrue/commit/f29e89d7d2c48ee8fd5bf8279a7fa3db0ad4d842))
## [2.143.0](https://github.com/supabase/gotrue/compare/v2.142.0...v2.143.0) (2024-02-19)
### Features
* calculate aal without transaction ([#1437](https://github.com/supabase/gotrue/issues/1437)) ([8dae661](https://github.com/supabase/gotrue/commit/8dae6614f1a2b58819f94894cef01e9f99117769))
### Bug Fixes
* deprecate hooks ([#1421](https://github.com/supabase/gotrue/issues/1421)) ([effef1b](https://github.com/supabase/gotrue/commit/effef1b6ecc448b7927eff23df8d5b509cf16b5c))
* error should be an IsNotFoundError ([#1432](https://github.com/supabase/gotrue/issues/1432)) ([7f40047](https://github.com/supabase/gotrue/commit/7f40047aec3577d876602444b1d88078b2237d66))
* populate password verification attempt hook ([#1436](https://github.com/supabase/gotrue/issues/1436)) ([f974bdb](https://github.com/supabase/gotrue/commit/f974bdb58340395955ca27bdd26d57062433ece9))
* restrict mfa enrollment to aal2 if verified factors are present ([#1439](https://github.com/supabase/gotrue/issues/1439)) ([7e10d45](https://github.com/supabase/gotrue/commit/7e10d45e54010d38677f4c3f2f224127688eb9a2))
* update phone if autoconfirm is enabled ([#1431](https://github.com/supabase/gotrue/issues/1431)) ([95db770](https://github.com/supabase/gotrue/commit/95db770c5d2ecca4a1e960a8cb28ded37cccc100))
* use email change email in identity ([#1429](https://github.com/supabase/gotrue/issues/1429)) ([4d3b9b8](https://github.com/supabase/gotrue/commit/4d3b9b8841b1a5fa8f3244825153cc81a73ba300))
## [2.142.0](https://github.com/supabase/gotrue/compare/v2.141.0...v2.142.0) (2024-02-14)
### Features
* alter tag to use raw ([#1427](https://github.com/supabase/gotrue/issues/1427)) ([53cfe5d](https://github.com/supabase/gotrue/commit/53cfe5de57d4b5ab6e8e2915493856ecd96f4ede))
* update README.md to trigger release ([#1425](https://github.com/supabase/gotrue/issues/1425)) ([91e0e24](https://github.com/supabase/gotrue/commit/91e0e245f5957ebce13370f79fd4a6be8108ed80))
## [2.141.0](https://github.com/supabase/gotrue/compare/v2.140.0...v2.141.0) (2024-02-13)
### Features
* drop sha hash tag ([#1422](https://github.com/supabase/gotrue/issues/1422)) ([76853ce](https://github.com/supabase/gotrue/commit/76853ce6d45064de5608acc8100c67a8337ba791))
* prefix release with v ([#1424](https://github.com/supabase/gotrue/issues/1424)) ([9d398cd](https://github.com/supabase/gotrue/commit/9d398cd75fca01fb848aa88b4f545552e8b5751a))
## [2.140.0](https://github.com/supabase/gotrue/compare/v2.139.2...v2.140.0) (2024-02-13)
### Features
* deprecate existing webhook implementation ([#1417](https://github.com/supabase/gotrue/issues/1417)) ([5301e48](https://github.com/supabase/gotrue/commit/5301e481b0c7278c18b4578a5b1aa8d2256c2f5d))
* update publish.yml checkout repository so there is access to Dockerfile ([#1419](https://github.com/supabase/gotrue/issues/1419)) ([7cce351](https://github.com/supabase/gotrue/commit/7cce3518e8c9f1f3f93e4f6a0658ee08771c4f1c))
## [2.139.2](https://github.com/supabase/gotrue/compare/v2.139.1...v2.139.2) (2024-02-08)
### Bug Fixes
* improve perf in account linking ([#1394](https://github.com/supabase/gotrue/issues/1394)) ([8eedb95](https://github.com/supabase/gotrue/commit/8eedb95dbaa310aac464645ec91d6a374813ab89))
* OIDC provider validation log message ([#1380](https://github.com/supabase/gotrue/issues/1380)) ([27e6b1f](https://github.com/supabase/gotrue/commit/27e6b1f9a4394c5c4f8dff9a8b5529db1fc67af9))
* only create or update the email / phone identity after it's been verified ([#1403](https://github.com/supabase/gotrue/issues/1403)) ([2d20729](https://github.com/supabase/gotrue/commit/2d207296ec22dd6c003c89626d255e35441fd52d))
* only create or update the email / phone identity after it's been verified (again) ([#1409](https://github.com/supabase/gotrue/issues/1409)) ([bc6a5b8](https://github.com/supabase/gotrue/commit/bc6a5b884b43fe6b8cb924d3f79999fe5bfe7c5f))
* unmarshal is_private_email correctly ([#1402](https://github.com/supabase/gotrue/issues/1402)) ([47df151](https://github.com/supabase/gotrue/commit/47df15113ce8d86666c0aba3854954c24fe39f7f))
* use `pattern` for semver docker image tags ([#1411](https://github.com/supabase/gotrue/issues/1411)) ([14a3aeb](https://github.com/supabase/gotrue/commit/14a3aeb6c3f46c8d38d98cc840112dfd0278eeda))
### Reverts
* "fix: only create or update the email / phone identity after i… ([#1407](https://github.com/supabase/gotrue/issues/1407)) ([ff86849](https://github.com/supabase/gotrue/commit/ff868493169a0d9ac18b66058a735197b1df5b9b))

1
auth_v2.169.0/CODEOWNERS Normal file
View File

@ -0,0 +1 @@
* @supabase/auth

View File

@ -0,0 +1,74 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at david@netlify.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -0,0 +1,523 @@
# CONTRIBUTING
We would love to have contributions from each and every one of you in the community be it big or small and you are the ones who motivate us to do better than what we do today.
## Code Of Conduct
Please help us keep all our projects open and inclusive. Kindly follow our [Code of Conduct](CODE_OF_CONDUCT.md) to keep the ecosystem healthy and friendly for all.
## Quick Start
Auth has a development container setup that makes it easy to get started contributing. This setup only requires that [Docker](https://www.docker.com/get-started) is setup on your system. The development container setup includes a PostgreSQL container with migrations already applied and a container running GoTrue that will perform a hot reload when changes to the source code are detected.
If you would like to run Auth locally or learn more about what these containers are doing for you, continue reading the [Setup and Tooling](#setup-and-tooling) section below. Otherwise, you can skip ahead to the [How To Verify that GoTrue is Available](#how-to-verify-that-auth-is-available) section to learn about working with and developing GoTrue.
Before using the containers, you will need to make sure an `.env.docker` file exists by making a copy of `example.docker.env` and configuring it for your needs. The set of env vars in `example.docker.env` only contain the necessary env vars for auth to start in a docker environment. For the full list of env vars, please refer to `example.env` and copy over the necessary ones into your `.env.docker` file.
The following are some basic commands. A full and up to date list of commands can be found in the project's `Makefile` or by running `make help`.
### Starting the containers
Start the containers as described above in an attached state with log output.
```bash
make dev
```
### Running tests in the containers
Start the containers with a fresh database and run the project's tests.
```bash
make docker-test
```
### Removing the containers
Remove both containers and their volumes. This removes any data associated with the containers.
```bash
make docker-clean
```
### Rebuild the containers
Fully rebuild the containers without using any cached layers.
```bash
make docker-build
```
## Setup and Tooling
Auth -- as the name implies -- is a user registration and authentication API developed in [Go](https://go.dev).
It connects to a [PostgreSQL](https://www.postgresql.org) database in order to store authentication data, [Soda CLI](https://gobuffalo.io/en/docs/db/toolbox) to manage database schema and migrations,
and runs inside a [Docker](https://www.docker.com/get-started) container.
Therefore, to contribute to Auth you will need to install these tools.
### Install Tools
- Install [Go](https://go.dev) 1.22
```zsh
# Via Homebrew on macOS
brew install go@1.22
# Set the environment variable in the ~/.zshrc file
echo 'export PATH="/opt/homebrew/opt/go@1.22/bin:$PATH"' >> ~/.zshrc
```
- Install [Docker](https://www.docker.com/get-started)
```zsh
# Via Homebrew on macOS
brew install docker
```
Or, if you prefer, download [Docker Desktop](https://www.docker.com/get-started).
- Install [Soda CLI](https://gobuffalo.io/en/docs/db/toolbox)
```zsh
# Via Homebrew on macOS
brew install gobuffalo/tap/pop
```
If you are on macOS Catalina you may [run into issues installing Soda with Brew](https://github.com/gobuffalo/homebrew-tap/issues/5). Do check your `GOPATH` and run
`go build -o /bin/soda github.com/gobuffalo/pop/soda` to resolve.
- Clone the Auth [repository](https://github.com/supabase/auth)
```zsh
git clone https://github.com/supabase/auth
```
### Install Auth
To begin installation, be sure to start from the root directory.
- `cd auth`
To complete installation, you will:
- Install the PostgreSQL Docker image
- Create the DB Schema and Migrations
- Setup a local `.env` for environment variables
- Compile Auth
- Run the Auth binary executable
#### Installation Steps
1. Start Docker
2. To install the PostgreSQL Docker image, run:
```zsh
# Builds the postgres image
docker-compose -f docker-compose-dev.yml build postgres
# Runs the postgres container
docker-compose -f docker-compose-dev.yml up postgres
```
You should then see in Docker that `auth_postgresql` is running on `port: 5432`.
> **Important** If you happen to already have a local running instance of Postgres running on the port `5432` because you
> may have installed via [homebrew on macOS](https://formulae.brew.sh/formula/postgresql) then be certain to stop the process using:
>
> - `brew services stop postgresql`
>
> If you need to run the test environment on another port, you will need to modify several configuration files to use a different custom port.
3. Next compile the Auth binary:
When you fork a repository, GitHub does not automatically copy all the tags (tags are not included by default). To ensure the correct tag is set before building the binary, you need to fetch the tags from the upstream repository and push them to your fork. Follow these steps:
```zsh
# Fetch the tags from the upstream repository
git fetch upstream --tags
# Push the tags to your fork
git push origin --tags
```
Then build the binary by running:
```zsh
make build
```
4. To setup the database schema via Soda, run:
```zsh
make migrate_test
```
You should see log messages that indicate that the Auth migrations were applied successfully:
```terminal
INFO[0000] Auth migrations applied successfully
DEBU[0000] after status
[POP] 2021/12/15 10:44:36 sql - SELECT EXISTS (SELECT schema_migrations.* FROM schema_migrations AS schema_migrations WHERE version = $1) | ["20210710035447"]
[POP] 2021/12/15 10:44:36 sql - SELECT EXISTS (SELECT schema_migrations.* FROM schema_migrations AS schema_migrations WHERE version = $1) | ["20210722035447"]
[POP] 2021/12/15 10:44:36 sql - SELECT EXISTS (SELECT schema_migrations.* FROM schema_migrations AS schema_migrations WHERE version = $1) | ["20210730183235"]
[POP] 2021/12/15 10:44:36 sql - SELECT EXISTS (SELECT schema_migrations.* FROM schema_migrations AS schema_migrations WHERE version = $1) | ["20210909172000"]
[POP] 2021/12/15 10:44:36 sql - SELECT EXISTS (SELECT schema_migrations.* FROM schema_migrations AS schema_migrations WHERE version = $1) | ["20211122151130"]
Version Name Status
20210710035447 alter_users Applied
20210722035447 adds_confirmed_at Applied
20210730183235 add_email_change_confirmed Applied
20210909172000 create_identities_table Applied
20211122151130 create_user_id_idx Applied
```
That lists each migration that was applied. Note: there may be more migrations than those listed.
4. Create a `.env` file in the root of the project and copy the following config in [example.env](example.env). Set the values to GOTRUE_SMS_TEST_OTP_VALID_UNTIL in the `.env` file.
5. In order to have Auth connect to your PostgreSQL database running in Docker, it is important to set a connection string like:
```
DATABASE_URL="postgres://supabase_auth_admin:root@localhost:5432/postgres"
```
> Important: Auth requires a set of SMTP credentials to run, you can generate your own SMTP credentials via an SMTP provider such as AWS SES, SendGrid, MailChimp, SendInBlue or any other SMTP providers.
6. Then finally Start Auth
7. Verify that Auth is Available
### Starting Auth
Start Auth by running the executable:
```zsh
./auth
```
This command will re-run migrations and then indicate that Auth has started:
```zsh
INFO[0000] Auth API started on: localhost:9999
```
### How To Verify that Auth is Available
To test that your Auth is up and available, you can query the `health` endpoint at `http://localhost:9999/health`. You should see a response similar to:
```json
{
"description": "Auth is a user registration and authentication API",
"name": "Auth",
"version": ""
}
```
To see the current settings, make a request to `http://localhost:9999/settings` and you should see a response similar to:
```json
{
"external": {
"apple": false,
"azure": false,
"bitbucket": false,
"discord": false,
"github": false,
"gitlab": false,
"google": false,
"facebook": false,
"spotify": false,
"slack": false,
"slack_oidc": false,
"twitch": true,
"twitter": false,
"email": true,
"phone": false,
"saml": false
},
"external_labels": {
"saml": "auth0"
},
"disable_signup": false,
"mailer_autoconfirm": false,
"phone_autoconfirm": false,
"sms_provider": "twilio"
}
```
## How to Use Admin API Endpoints
To test the admin endpoints (or other api endpoints), you can invoke via HTTP requests. Using [Insomnia](https://insomnia.rest/products/insomnia) can help you issue these requests.
You will need to know the `GOTRUE_JWT_SECRET` configured in the `.env` settings.
Also, you must generate a JWT with the signature which has the `supabase_admin` role (or one that is specified in `GOTRUE_JWT_ADMIN_ROLES`).
For example:
```json
{
"role": "supabase_admin"
}
```
You can sign this payload using the [JWT.io Debugger](https://jwt.io/#debugger-io) but make sure that `secret base64 encoded` is unchecked.
Then you can use this JWT as a Bearer token for admin requests.
### Create User (aka Sign Up a User)
To create a new user, `POST /admin/users` with the payload:
```json
{
"email": "user@example.com",
"password": "12345678"
}
```
#### Request
```
POST /admin/users HTTP/1.1
Host: localhost:9999
User-Agent: insomnia/2021.7.2
Content-Type: application/json
Authorization: Bearer <YOUR_SIGNED_JWT>
Accept: */*
Content-Length: 57
```
#### Response
And you should get a new user:
```json
{
"id": "e78c512d-68e4-482b-901b-75003e89acae",
"aud": "authenticated",
"role": "authenticated",
"email": "user@example.com",
"phone": "",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {},
"identities": null,
"created_at": "2021-12-15T12:40:03.507551-05:00",
"updated_at": "2021-12-15T12:40:03.512067-05:00"
}
```
### List/Find Users
To create a new user, make a request to `GET /admin/users`.
#### Request
```
GET /admin/users HTTP/1.1
Host: localhost:9999
User-Agent: insomnia/2021.7.2
Authorization: Bearer <YOUR*SIGNED_JWT>
Accept: */\_
```
#### Response
The response from `/admin/users` should return all users:
```json
{
"aud": "authenticated",
"users": [
{
"id": "b7fd0253-6e16-4d4e-b61b-5943cb1b2102",
"aud": "authenticated",
"role": "authenticated",
"email": "user+4@example.com",
"phone": "",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {},
"identities": null,
"created_at": "2021-12-15T12:43:58.12207-05:00",
"updated_at": "2021-12-15T12:43:58.122073-05:00"
},
{
"id": "d69ae847-99be-4642-868f-439c2cdd9af4",
"aud": "authenticated",
"role": "authenticated",
"email": "user+3@example.com",
"phone": "",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {},
"identities": null,
"created_at": "2021-12-15T12:43:56.730209-05:00",
"updated_at": "2021-12-15T12:43:56.730213-05:00"
},
{
"id": "7282cf42-344e-4474-bdf6-d48e4968a2e4",
"aud": "authenticated",
"role": "authenticated",
"email": "user+2@example.com",
"phone": "",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {},
"identities": null,
"created_at": "2021-12-15T12:43:54.867676-05:00",
"updated_at": "2021-12-15T12:43:54.867679-05:00"
},
{
"id": "e78c512d-68e4-482b-901b-75003e89acae",
"aud": "authenticated",
"role": "authenticated",
"email": "user@example.com",
"phone": "",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {},
"identities": null,
"created_at": "2021-12-15T12:40:03.507551-05:00",
"updated_at": "2021-12-15T12:40:03.507554-05:00"
}
]
}
```
### Running Database Migrations
If you need to run any new migrations:
```zsh
make migrate_test
```
## Testing
Currently, we don't use a separate test database, so the same database created when installing Auth to run locally is used.
The following commands should help in setting up a database and running the tests:
```sh
# Runs the database in a docker container
$ docker-compose -f docker-compose-dev.yml up postgres
# Applies the migrations to the database (requires soda cli)
$ make migrate_test
# Executes the tests
$ make test
```
### Customizing the PostgreSQL Port
if you already run PostgreSQL and need to run your database on a different, custom port,
you will need to make several configuration changes to the following files:
In these examples, we change the port from 5432 to 7432.
> Note: This is not recommended, but if you do, please do not check in changes.
```
// file: docker-compose-dev.yml
ports:
- 7432:5432 \ 👈 set the first value to your external facing port
```
The port you customize here can them be used in the subsequent configuration:
```
// file: database.yaml
test:
dialect: "postgres"
database: "postgres"
host: {{ envOr "POSTGRES_HOST" "127.0.0.1" }}
port: {{ envOr "POSTGRES_PORT" "7432" }} 👈 set to your port
```
```
// file: test.env
DATABASE_URL="postgres://supabase_auth_admin:root@localhost:7432/postgres" 👈 set to your port
```
```
// file: migrate.sh
export GOTRUE_DB_DATABASE_URL="postgres://supabase_auth_admin:root@localhost:7432/$DB_ENV"
```
## Helpful Docker Commands
```
// file: docker-compose-dev.yml
container_name: auth_postgres
```
```zsh
# Command line into bash on the PostgreSQL container
docker exec -it auth_postgres bash
# Removes Container
docker container rm -f auth_postgres
# Removes volume
docker volume rm postgres_data
```
## Updating Package Dependencies
- `make deps`
- `go mod tidy` if necessary
## Submitting Pull Requests
We actively welcome your pull requests.
- Fork the repo and create your branch from `master`.
- If you've added code that should be tested, add tests.
- If you've changed APIs, update the documentation.
- Ensure the test suite passes.
- Make sure your code lints.
### Checklist for Submitting Pull Requests
- Is there a corresponding issue created for it? If so, please include it in the PR description so we can track / refer to it.
- Does your PR follow the [semantic-release commit guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines)?
- If the PR is a `feat`, an [RFC](https://github.com/supabase/rfcs) or a detailed description of the design implementation is required. The former (RFC) is preferred before starting on the PR.
- Are the existing tests passing?
- Have you written some tests for your PR?
## Guidelines for Implementing Additional OAuth Providers
> ⚠️ We won't be accepting any additional oauth / sms provider contributions for now because we intend to support these through webhooks or a generic provider in the future.
Please ensure that an end-to-end test is done for the OAuth provider implemented.
An end-to-end test includes:
- Creating an application on the oauth provider site
- Generating your own client_id and secret
- Testing that `http://localhost:9999/authorize?provider=MY_COOL_NEW_PROVIDER` redirects you to the provider sign-in page
- The callback is handled properly
- Gotrue redirects to the `SITE_URL` or one of the URI's specified in the `URI_ALLOW_LIST` with the access_token, provider_token, expiry and refresh_token as query fragments
### Writing tests for the new OAuth provider implemented
Since implementing an additional OAuth provider consists of making api calls to an external api, we set up a mock server to attempt to mock the responses expected from the OAuth provider.
## License
By contributing to Auth, you agree that your contributions will be licensed
under its [MIT license](LICENSE).

32
auth_v2.169.0/Dockerfile Normal file
View File

@ -0,0 +1,32 @@
FROM golang:1.22.3-alpine3.20 as build
ENV GO111MODULE=on
ENV CGO_ENABLED=0
ENV GOOS=linux
RUN apk add --no-cache make git
WORKDIR /go/src/github.com/supabase/auth
# Pulling dependencies
COPY ./Makefile ./go.* ./
RUN make deps
# Building stuff
COPY . /go/src/github.com/supabase/auth
# Make sure you change the RELEASE_VERSION value before publishing an image.
RUN RELEASE_VERSION=unspecified make build
# Always use alpine:3 so the latest version is used. This will keep CA certs more up to date.
FROM alpine:3
RUN adduser -D -u 1000 supabase
RUN apk add --no-cache ca-certificates
COPY --from=build /go/src/github.com/supabase/auth/auth /usr/local/bin/auth
COPY --from=build /go/src/github.com/supabase/auth/migrations /usr/local/etc/auth/migrations/
RUN ln -s /usr/local/bin/auth /usr/local/bin/gotrue
ENV GOTRUE_DB_MIGRATIONS_PATH /usr/local/etc/auth/migrations
USER supabase
CMD ["auth"]

View File

@ -0,0 +1,18 @@
FROM golang:1.22.3-alpine3.20
ENV GO111MODULE=on
ENV CGO_ENABLED=0
ENV GOOS=linux
RUN apk add --no-cache make git bash
WORKDIR /go/src/github.com/supabase/auth
# Pulling dependencies
COPY ./Makefile ./go.* ./
# Production dependencies
RUN make deps
# Development dependences
RUN go get github.com/githubnemo/CompileDaemon
RUN go install github.com/githubnemo/CompileDaemon

View File

@ -0,0 +1,8 @@
FROM postgres:15
WORKDIR /
RUN pwd
COPY init_postgres.sh /docker-entrypoint-initdb.d/init.sh
RUN chmod +x /docker-entrypoint-initdb.d/init.sh
EXPOSE 5432
CMD ["postgres"]

21
auth_v2.169.0/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Supabase <support@supabase.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

93
auth_v2.169.0/Makefile Normal file
View File

@ -0,0 +1,93 @@
.PHONY: all build deps dev-deps image migrate test vet sec format unused
CHECK_FILES?=./...
FLAGS=-ldflags "-X github.com/supabase/auth/internal/utilities.Version=`git describe --tags`" -buildvcs=false
ifdef RELEASE_VERSION
FLAGS=-ldflags "-X github.com/supabase/auth/internal/utilities.Version=v$(RELEASE_VERSION)" -buildvcs=false
endif
ifneq ($(shell docker compose version 2>/dev/null),)
DOCKER_COMPOSE=docker compose
else
DOCKER_COMPOSE=docker-compose
endif
DEV_DOCKER_COMPOSE:=docker-compose-dev.yml
help: ## Show this help.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
all: vet sec static build ## Run the tests and build the binary.
build: deps ## Build the binary.
CGO_ENABLED=0 go build $(FLAGS)
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build $(FLAGS) -o auth-arm64
dev-deps: ## Install developer dependencies
@go install github.com/gobuffalo/pop/soda@latest
@go install github.com/securego/gosec/v2/cmd/gosec@latest
@go install honnef.co/go/tools/cmd/staticcheck@latest
@go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest
@go install github.com/nishanths/exhaustive/cmd/exhaustive@latest
deps: ## Install dependencies.
@go mod download
@go mod verify
migrate_dev: ## Run database migrations for development.
hack/migrate.sh postgres
migrate_test: ## Run database migrations for test.
hack/migrate.sh postgres
test: build ## Run tests.
go test $(CHECK_FILES) -coverprofile=coverage.out -coverpkg ./... -p 1 -race -v -count=1
./hack/coverage.sh
vet: # Vet the code
go vet $(CHECK_FILES)
sec: dev-deps # Check for security vulnerabilities
gosec -quiet -exclude-generated $(CHECK_FILES)
gosec -quiet -tests -exclude-generated -exclude=G104 $(CHECK_FILES)
unused: dev-deps # Look for unused code
@echo "Unused code:"
staticcheck -checks U1000 $(CHECK_FILES)
@echo
@echo "Code used only in _test.go (do move it in those files):"
staticcheck -checks U1000 -tests=false $(CHECK_FILES)
static: dev-deps
staticcheck ./...
exhaustive ./...
generate: dev-deps
go generate ./...
dev: ## Run the development containers
${DOCKER_COMPOSE} -f $(DEV_DOCKER_COMPOSE) up
down: ## Shutdown the development containers
# Start postgres first and apply migrations
${DOCKER_COMPOSE} -f $(DEV_DOCKER_COMPOSE) down
docker-test: ## Run the tests using the development containers
${DOCKER_COMPOSE} -f $(DEV_DOCKER_COMPOSE) up -d postgres
${DOCKER_COMPOSE} -f $(DEV_DOCKER_COMPOSE) run auth sh -c "make migrate_test"
${DOCKER_COMPOSE} -f $(DEV_DOCKER_COMPOSE) run auth sh -c "make test"
${DOCKER_COMPOSE} -f $(DEV_DOCKER_COMPOSE) down -v
docker-build: ## Force a full rebuild of the development containers
${DOCKER_COMPOSE} -f $(DEV_DOCKER_COMPOSE) build --no-cache
${DOCKER_COMPOSE} -f $(DEV_DOCKER_COMPOSE) up -d postgres
${DOCKER_COMPOSE} -f $(DEV_DOCKER_COMPOSE) run auth sh -c "make migrate_dev"
${DOCKER_COMPOSE} -f $(DEV_DOCKER_COMPOSE) down
docker-clean: ## Remove the development containers and volumes
${DOCKER_COMPOSE} -f $(DEV_DOCKER_COMPOSE) rm -fsv
format:
gofmt -s -w .

1229
auth_v2.169.0/README.md Normal file

File diff suppressed because it is too large Load Diff

60
auth_v2.169.0/SECURITY.md Normal file
View File

@ -0,0 +1,60 @@
# Security Policy
Auth is a project maintained by [Supabase](https://supabase.com). Below is
our security policy.
Contact: security@supabase.io
Canonical: https://supabase.com/.well-known/security.txt
At Supabase, we consider the security of our systems a top priority. But no
matter how much effort we put into system security, there can still be
vulnerabilities present.
If you discover a vulnerability, we would like to know about it so we can take
steps to address it as quickly as possible. We would like to ask you to help us
better protect our clients and our systems.
Out of scope vulnerabilities:
- Clickjacking on pages with no sensitive actions.
- Unauthenticated/logout/login CSRF.
- Attacks requiring MITM or physical access to a user's device.
- Any activity that could lead to the disruption of our service (DoS).
- Content spoofing and text injection issues without showing an attack
vector/without being able to modify HTML/CSS.
- Email spoofing
- Missing DNSSEC, CAA, CSP headers
- Lack of Secure or HTTP only flag on non-sensitive cookies
- Deadlinks
Please do the following:
- E-mail your findings to security@supabase.io.
- Do not run automated scanners on our infrastructure or dashboard. If you wish
to do this, contact us and we will set up a sandbox for you.
- Do not take advantage of the vulnerability or problem you have discovered,
for example by downloading more data than necessary to demonstrate the
vulnerability or deleting or modifying other people's data,
- Do not reveal the problem to others until it has been resolved,
- Do not use attacks on physical security, social engineering, distributed
denial of service, spam or applications of third parties, and
- Do provide sufficient information to reproduce the problem, so we will be
able to resolve it as quickly as possible. Usually, the IP address or the URL
of the affected system and a description of the vulnerability will be
sufficient, but complex vulnerabilities may require further explanation.
What we promise:
- We will respond to your report within 3 business days with our evaluation of
the report and an expected resolution date,
- If you have followed the instructions above, we will not take any legal
action against you in regard to the report,
- We will handle your report with strict confidentiality, and not pass on your
personal details to third parties without your permission,
- We will keep you informed of the progress towards resolving the problem,
- In the public information concerning the problem reported, we will give your
name as the discoverer of the problem (unless you desire otherwise), and
We strive to resolve all problems as quickly as possible, and we would like to
play an active role in the ultimate publication on the problem after it is
resolved.

34
auth_v2.169.0/app.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "Gotrue",
"description": "",
"website": "https://www.gotrueapi.org",
"repository": "https://github.com/supabase/gotrue",
"env": {
"DATABASE_URL": {},
"GOTRUE_DB_DRIVER": {
"value": "postgres"
},
"GOTRUE_DB_AUTOMIGRATE": {
"value": true
},
"GOTRUE_DB_NAMESPACE": {
"value": "auth"
},
"GOTRUE_JWT_SECRET": {
"required": true
},
"GOTRUE_SMTP_ADMIN_EMAIL": {},
"GOTRUE_SMTP_HOST": {},
"GOTRUE_SMTP_PASS": {},
"GOTRUE_SMTP_PORT": {},
"GOTRUE_MAILER_SITE_URL": {},
"GOTRUE_MAILER_SUBJECTS_CONFIRMATION": {},
"GOTRUE_MAILER_SUBJECTS_RECOVERY": {},
"GOTRUE_MAILER_SUBJECTS_MAGIC_LINK": {},
"GOTRUE_MAILER_TEMPLATES_CONFIRMATION": {},
"GOTRUE_MAILER_TEMPLATES_EMAIL_CHANGE": {},
"GOTRUE_MAILER_TEMPLATES_RECOVERY": {},
"GOTRUE_MAILER_TEMPLATES_MAGIC_LINK": {},
"GOTRUE_MAILER_USER": {}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
package admin
//go:generate oapi-codegen -config ./oapi-codegen.yaml ../../openapi.yaml

View File

@ -0,0 +1,7 @@
package: admin
generate:
- client
- types
include-tags:
- admin
output: client.go

View File

@ -0,0 +1,131 @@
package cmd
import (
"github.com/gofrs/uuid"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/storage"
)
var autoconfirm, isAdmin bool
var audience string
func getAudience(c *conf.GlobalConfiguration) string {
if audience == "" {
return c.JWT.Aud
}
return audience
}
func adminCmd() *cobra.Command {
var adminCmd = &cobra.Command{
Use: "admin",
}
adminCmd.AddCommand(&adminCreateUserCmd, &adminDeleteUserCmd)
adminCmd.PersistentFlags().StringVarP(&audience, "aud", "a", "", "Set the new user's audience")
adminCreateUserCmd.Flags().BoolVar(&autoconfirm, "confirm", false, "Automatically confirm user without sending an email")
adminCreateUserCmd.Flags().BoolVar(&isAdmin, "admin", false, "Create user with admin privileges")
return adminCmd
}
var adminCreateUserCmd = cobra.Command{
Use: "createuser",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 2 {
logrus.Fatal("Not enough arguments to createuser command. Expected at least email and password values")
return
}
execWithConfigAndArgs(cmd, adminCreateUser, args)
},
}
var adminDeleteUserCmd = cobra.Command{
Use: "deleteuser",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
logrus.Fatal("Not enough arguments to deleteuser command. Expected at least ID or email")
return
}
execWithConfigAndArgs(cmd, adminDeleteUser, args)
},
}
func adminCreateUser(config *conf.GlobalConfiguration, args []string) {
db, err := storage.Dial(config)
if err != nil {
logrus.Fatalf("Error opening database: %+v", err)
}
defer db.Close()
aud := getAudience(config)
if user, err := models.IsDuplicatedEmail(db, args[0], aud, nil); user != nil {
logrus.Fatalf("Error creating new user: user already exists")
} else if err != nil {
logrus.Fatalf("Error checking user email: %+v", err)
}
user, err := models.NewUser("", args[0], args[1], aud, nil)
if err != nil {
logrus.Fatalf("Error creating new user: %+v", err)
}
err = db.Transaction(func(tx *storage.Connection) error {
var terr error
if terr = tx.Create(user); terr != nil {
return terr
}
if len(args) > 2 {
if terr = user.SetRole(tx, args[2]); terr != nil {
return terr
}
} else if isAdmin {
if terr = user.SetRole(tx, config.JWT.AdminGroupName); terr != nil {
return terr
}
}
if config.Mailer.Autoconfirm || autoconfirm {
if terr = user.Confirm(tx); terr != nil {
return terr
}
}
return nil
})
if err != nil {
logrus.Fatalf("Unable to create user (%s): %+v", args[0], err)
}
logrus.Infof("Created user: %s", args[0])
}
func adminDeleteUser(config *conf.GlobalConfiguration, args []string) {
db, err := storage.Dial(config)
if err != nil {
logrus.Fatalf("Error opening database: %+v", err)
}
defer db.Close()
user, err := models.FindUserByEmailAndAudience(db, args[0], getAudience(config))
if err != nil {
userID := uuid.Must(uuid.FromString(args[0]))
user, err = models.FindUserByID(db, userID)
if err != nil {
logrus.Fatalf("Error finding user (%s): %+v", userID, err)
}
}
if err = db.Destroy(user); err != nil {
logrus.Fatalf("Error removing user (%s): %+v", args[0], err)
}
logrus.Infof("Removed user: %s", args[0])
}

View File

@ -0,0 +1,117 @@
package cmd
import (
"embed"
"fmt"
"net/url"
"os"
"github.com/gobuffalo/pop/v6"
"github.com/gobuffalo/pop/v6/logging"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var EmbeddedMigrations embed.FS
var migrateCmd = cobra.Command{
Use: "migrate",
Long: "Migrate database strucutures. This will create new tables and add missing columns and indexes.",
Run: migrate,
}
func migrate(cmd *cobra.Command, args []string) {
globalConfig := loadGlobalConfig(cmd.Context())
if globalConfig.DB.Driver == "" && globalConfig.DB.URL != "" {
u, err := url.Parse(globalConfig.DB.URL)
if err != nil {
logrus.Fatalf("%+v", errors.Wrap(err, "parsing db connection url"))
}
globalConfig.DB.Driver = u.Scheme
}
log := logrus.StandardLogger()
pop.Debug = false
if globalConfig.Logging.Level != "" {
level, err := logrus.ParseLevel(globalConfig.Logging.Level)
if err != nil {
log.Fatalf("Failed to parse log level: %+v", err)
}
log.SetLevel(level)
if level == logrus.DebugLevel {
// Set to true to display query info
pop.Debug = true
}
if level != logrus.DebugLevel {
var noopLogger = func(lvl logging.Level, s string, args ...interface{}) {
}
// Hide pop migration logging
pop.SetLogger(noopLogger)
}
}
u, _ := url.Parse(globalConfig.DB.URL)
processedUrl := globalConfig.DB.URL
if len(u.Query()) != 0 {
processedUrl = fmt.Sprintf("%s&application_name=gotrue_migrations", processedUrl)
} else {
processedUrl = fmt.Sprintf("%s?application_name=gotrue_migrations", processedUrl)
}
deets := &pop.ConnectionDetails{
Dialect: globalConfig.DB.Driver,
URL: processedUrl,
}
deets.Options = map[string]string{
"migration_table_name": "schema_migrations",
"Namespace": globalConfig.DB.Namespace,
}
db, err := pop.NewConnection(deets)
if err != nil {
log.Fatalf("%+v", errors.Wrap(err, "opening db connection"))
}
defer db.Close()
if err := db.Open(); err != nil {
log.Fatalf("%+v", errors.Wrap(err, "checking database connection"))
}
log.Debugf("Reading migrations from executable")
box, err := pop.NewMigrationBox(EmbeddedMigrations, db)
if err != nil {
log.Fatalf("%+v", errors.Wrap(err, "creating db migrator"))
}
mig := box.Migrator
log.Debugf("before status")
if log.Level == logrus.DebugLevel {
err = mig.Status(os.Stdout)
if err != nil {
log.Fatalf("%+v", errors.Wrap(err, "migration status"))
}
}
// turn off schema dump
mig.SchemaPath = ""
err = mig.Up()
if err != nil {
log.Fatalf("%v", errors.Wrap(err, "running db migrations"))
} else {
log.Infof("GoTrue migrations applied successfully")
}
log.Debugf("after status")
if log.Level == logrus.DebugLevel {
err = mig.Status(os.Stdout)
if err != nil {
log.Fatalf("%+v", errors.Wrap(err, "migration status"))
}
}
}

View File

@ -0,0 +1,63 @@
package cmd
import (
"context"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/observability"
)
var (
configFile = ""
watchDir = ""
)
var rootCmd = cobra.Command{
Use: "gotrue",
Run: func(cmd *cobra.Command, args []string) {
migrate(cmd, args)
serve(cmd.Context())
},
}
// RootCommand will setup and return the root command
func RootCommand() *cobra.Command {
rootCmd.AddCommand(&serveCmd, &migrateCmd, &versionCmd, adminCmd())
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "base configuration file to load")
rootCmd.PersistentFlags().StringVarP(&watchDir, "config-dir", "d", "", "directory containing a sorted list of config files to watch for changes")
return &rootCmd
}
func loadGlobalConfig(ctx context.Context) *conf.GlobalConfiguration {
if ctx == nil {
panic("context must not be nil")
}
config, err := conf.LoadGlobal(configFile)
if err != nil {
logrus.Fatalf("Failed to load configuration: %+v", err)
}
if err := observability.ConfigureLogging(&config.Logging); err != nil {
logrus.WithError(err).Error("unable to configure logging")
}
if err := observability.ConfigureTracing(ctx, &config.Tracing); err != nil {
logrus.WithError(err).Error("unable to configure tracing")
}
if err := observability.ConfigureMetrics(ctx, &config.Metrics); err != nil {
logrus.WithError(err).Error("unable to configure metrics")
}
if err := observability.ConfigureProfiler(ctx, &config.Profiler); err != nil {
logrus.WithError(err).Error("unable to configure profiler")
}
return config
}
func execWithConfigAndArgs(cmd *cobra.Command, fn func(config *conf.GlobalConfiguration, args []string), args []string) {
fn(loadGlobalConfig(cmd.Context()), args)
}

View File

@ -0,0 +1,111 @@
package cmd
import (
"context"
"net"
"net/http"
"sync"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/supabase/auth/internal/api"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/reloader"
"github.com/supabase/auth/internal/storage"
"github.com/supabase/auth/internal/utilities"
)
var serveCmd = cobra.Command{
Use: "serve",
Long: "Start API server",
Run: func(cmd *cobra.Command, args []string) {
serve(cmd.Context())
},
}
func serve(ctx context.Context) {
if err := conf.LoadFile(configFile); err != nil {
logrus.WithError(err).Fatal("unable to load config")
}
if err := conf.LoadDirectory(watchDir); err != nil {
logrus.WithError(err).Fatal("unable to load config from watch dir")
}
config, err := conf.LoadGlobalFromEnv()
if err != nil {
logrus.WithError(err).Fatal("unable to load config")
}
db, err := storage.Dial(config)
if err != nil {
logrus.Fatalf("error opening database: %+v", err)
}
defer db.Close()
addr := net.JoinHostPort(config.API.Host, config.API.Port)
logrus.Infof("GoTrue API started on: %s", addr)
opts := []api.Option{
api.NewLimiterOptions(config),
}
a := api.NewAPIWithVersion(config, db, utilities.Version, opts...)
ah := reloader.NewAtomicHandler(a)
baseCtx, baseCancel := context.WithCancel(context.Background())
defer baseCancel()
httpSrv := &http.Server{
Addr: addr,
Handler: ah,
ReadHeaderTimeout: 2 * time.Second, // to mitigate a Slowloris attack
BaseContext: func(net.Listener) context.Context {
return baseCtx
},
}
log := logrus.WithField("component", "api")
var wg sync.WaitGroup
defer wg.Wait() // Do not return to caller until this goroutine is done.
if watchDir != "" {
wg.Add(1)
go func() {
defer wg.Done()
fn := func(latestCfg *conf.GlobalConfiguration) {
log.Info("reloading api with new configuration")
latestAPI := api.NewAPIWithVersion(
latestCfg, db, utilities.Version, opts...)
ah.Store(latestAPI)
}
rl := reloader.NewReloader(watchDir)
if err := rl.Watch(ctx, fn); err != nil {
log.WithError(err).Error("watcher is exiting")
}
}()
}
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
defer baseCancel() // close baseContext
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), time.Minute)
defer shutdownCancel()
if err := httpSrv.Shutdown(shutdownCtx); err != nil && !errors.Is(err, context.Canceled) {
log.WithError(err).Error("shutdown failed")
}
}()
if err := httpSrv.ListenAndServe(); err != http.ErrServerClosed {
log.WithError(err).Fatal("http server listen failed")
}
}

View File

@ -0,0 +1,17 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/supabase/auth/internal/utilities"
)
var versionCmd = cobra.Command{
Run: showVersion,
Use: "version",
}
func showVersion(cmd *cobra.Command, args []string) {
fmt.Println(utilities.Version)
}

View File

@ -0,0 +1,34 @@
version: "3.9"
services:
auth:
container_name: auth
depends_on:
- postgres
build:
context: ./
dockerfile: Dockerfile.dev
ports:
- '9999:9999'
- '9100:9100'
environment:
- GOTRUE_DB_MIGRATIONS_PATH=/go/src/github.com/supabase/auth/migrations
volumes:
- ./:/go/src/github.com/supabase/auth
command: CompileDaemon --build="make build" --directory=/go/src/github.com/supabase/auth --recursive=true -pattern="(.+\.go|.+\.env)" -exclude=auth -exclude=auth-arm64 -exclude=.env --command="/go/src/github.com/supabase/auth/auth -c=.env.docker"
postgres:
build:
context: .
dockerfile: Dockerfile.postgres.dev
container_name: auth_postgres
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=root
- POSTGRES_DB=postgres
# sets the schema name, this should match the `NAMESPACE` env var set in your .env file
- DB_NAMESPACE=auth
volumes:
postgres_data:

106
auth_v2.169.0/docs/admin.go Normal file
View File

@ -0,0 +1,106 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
import (
"github.com/supabase/auth/internal/api"
)
// swagger:route GET /admin/users admin admin-list-users
// List all users.
// security:
// - bearer:
// responses:
// 200: adminListUserResponse
// 401: unauthorizedError
// The list of users.
// swagger:response adminListUserResponse
type adminListUserResponseWrapper struct {
// in:body
Body api.AdminListUsersResponse
}
// swagger:route POST /admin/users admin admin-create-user
// Returns the created user.
// security:
// - bearer:
// responses:
// 200: userResponse
// 401: unauthorizedError
// The user to be created.
// swagger:parameters admin-create-user
type adminUserParamsWrapper struct {
// in:body
Body api.AdminUserParams
}
// swagger:route GET /admin/user/{user_id} admin admin-get-user
// Get a user.
// security:
// - bearer:
// parameters:
// + name: user_id
// in: path
// description: The user's id
// required: true
// responses:
// 200: userResponse
// 401: unauthorizedError
// The user specified.
// swagger:response userResponse
// swagger:route PUT /admin/user/{user_id} admin admin-update-user
// Update a user.
// security:
// - bearer:
// parameters:
// + name: user_id
// in: path
// description: The user's id
// required: true
// responses:
// 200: userResponse
// 401: unauthorizedError
// The updated user.
// swagger:response userResponse
// swagger:route DELETE /admin/user/{user_id} admin admin-delete-user
// Deletes a user.
// security:
// - bearer:
// parameters:
// + name: user_id
// in: path
// description: The user's id
// required: true
// responses:
// 200: deleteUserResponse
// 401: unauthorizedError
// The updated user.
// swagger:response deleteUserResponse
type deleteUserResponseWrapper struct{}
// swagger:route POST /admin/generate_link admin admin-generate-link
// Generates an email action link.
// security:
// - bearer:
// responses:
// 200: generateLinkResponse
// 401: unauthorizedError
// swagger:parameters admin-generate-link
type generateLinkParams struct {
// in:body
Body api.GenerateLinkParams
}
// The response object for generate link.
// swagger:response generateLinkResponse
type generateLinkResponseWrapper struct {
// in:body
Body api.GenerateLinkResponse
}

20
auth_v2.169.0/docs/doc.go Normal file
View File

@ -0,0 +1,20 @@
// Package classification gotrue
//
// Documentation of the gotrue API.
//
// Schemes: http, https
// BasePath: /
// Version: 1.0.0
// Host: localhost:9999
//
// SecurityDefinitions:
// bearer:
// type: apiKey
// name: Authentication
// in: header
//
// Produces:
// - application/json
//
// swagger:meta
package docs

View File

@ -0,0 +1,6 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
// This endpoint requires a bearer token.
// swagger:response unauthorizedError
type unauthorizedError struct{}

View File

@ -0,0 +1,15 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
import "github.com/supabase/auth/internal/api"
// swagger:route GET /health health health
// The healthcheck endpoint for gotrue. Returns the current gotrue version.
// responses:
// 200: healthCheckResponse
// swagger:response healthCheckResponse
type healthCheckResponseWrapper struct {
// in:body
Body api.HealthCheckResponse
}

View File

@ -0,0 +1,18 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
import "github.com/supabase/auth/internal/api"
// swagger:route POST /invite invite invite
// Sends an invite link to the user.
// responses:
// 200: inviteResponse
// swagger:parameters invite
type inviteParamsWrapper struct {
// in:body
Body api.InviteParams
}
// swagger:response inviteResponse
type inviteResponseWrapper struct{}

View File

@ -0,0 +1,12 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
// swagger:route POST /logout logout logout
// Logs out the user.
// security:
// - bearer:
// responses:
// 204: logoutResponse
// swagger:response logoutResponse
type logoutResponseWrapper struct{}

View File

@ -0,0 +1,25 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
// swagger:route GET /authorize oauth authorize
// Redirects the user to the 3rd-party OAuth provider to start the OAuth1.0 or OAuth2.0 authentication process.
// parameters:
// + name: redirect_to
// in: query
// description: The redirect url to return the user to after the `/callback` endpoint has completed.
// required: false
// responses:
// 302: authorizeResponse
// Redirects user to the 3rd-party OAuth provider
// swagger:response authorizeResponse
type authorizeResponseWrapper struct{}
// swagger:route GET /callback oauth callback
// Receives the redirect from an external provider during the OAuth authentication process. Starts the process of creating an access and refresh token.
// responses:
// 302: callbackResponse
// Redirects user to the redirect url specified in `/authorize`. If no `redirect_url` is provided, the user will be redirected to the `SITE_URL`.
// swagger:response callbackResponse
type callbackResponseWrapper struct{}

19
auth_v2.169.0/docs/otp.go Normal file
View File

@ -0,0 +1,19 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
import "github.com/supabase/auth/internal/api"
// swagger:route POST /otp otp otp
// Passwordless sign-in method for email or phone.
// responses:
// 200: otpResponse
// swagger:parameters otp
type otpParamsWrapper struct {
// Only an email or phone should be provided.
// in:body
Body api.OtpParams
}
// swagger:response otpResponse
type otpResponseWrapper struct{}

View File

@ -0,0 +1,18 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
import "github.com/supabase/auth/internal/api"
// swagger:route POST /recover recovery recovery
// Sends a password recovery email link to the user's email.
// responses:
// 200: recoveryResponse
// swagger:parameters recovery
type recoveryParamsWrapper struct {
// in:body
Body api.RecoverParams
}
// swagger:response recoveryResponse
type recoveryResponseWrapper struct{}

View File

@ -0,0 +1,15 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
import "github.com/supabase/auth/internal/api"
// swagger:route GET /settings settings settings
// Returns the configuration settings for the gotrue server.
// responses:
// 200: settingsResponse
// swagger:response settingsResponse
type settingsResponseWrapper struct {
// in:body
Body api.Settings
}

View File

@ -0,0 +1,17 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
import (
"github.com/supabase/auth/internal/api"
)
// swagger:route POST /signup signup signup
// Password-based signup with either email or phone.
// responses:
// 200: userResponse
// swagger:parameters signup
type signupParamsWrapper struct {
// in:body
Body api.SignupParams
}

View File

@ -0,0 +1,34 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
import (
"github.com/supabase/auth/internal/api"
)
// swagger:route POST /token?grant_type=password token token-password
// Signs in a user with a password.
// responses:
// 200: tokenResponse
// swagger:parameters token-password
type tokenPasswordGrantParamsWrapper struct {
// in:body
Body api.PasswordGrantParams
}
// swagger:route POST /token?grant_type=refresh_token token token-refresh
// Refreshes a user's refresh token.
// responses:
// 200: tokenResponse
// swagger:parameters token-refresh
type tokenRefreshTokenGrantParamsWrapper struct {
// in:body
Body api.RefreshTokenGrantParams
}
// swagger:response tokenResponse
type tokenResponseWrapper struct {
// in:body
Body api.AccessTokenResponse
}

View File

@ -0,0 +1,37 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
import (
"github.com/supabase/auth/internal/api"
"github.com/supabase/auth/internal/models"
)
// swagger:route GET /user user user-get
// Get information for the logged-in user.
// security:
// - bearer:
// responses:
// 200: userResponse
// 401: unauthorizedError
// The current user.
// swagger:response userResponse
type userResponseWrapper struct {
// in:body
Body models.User
}
// swagger:route PUT /user user user-put
// Returns the updated user.
// security:
// - bearer:
// responses:
// 200: userResponse
// 401: unauthorizedError
// The current user.
// swagger:parameters user-put
type userUpdateParams struct {
// in:body
Body api.UserUpdateParams
}

View File

@ -0,0 +1,24 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs
import (
"github.com/supabase/auth/internal/api"
)
// swagger:route GET /verify verify verify-get
// Verifies a sign up.
// swagger:parameters verify-get
type verifyGetParamsWrapper struct {
// in:query
api.VerifyParams
}
// swagger:route POST /verify verify verify-post
// Verifies a sign up.
// swagger:parameters verify-post
type verifyPostParamsWrapper struct {
// in:body
Body api.VerifyParams
}

View File

@ -0,0 +1,8 @@
GOTRUE_SITE_URL="http://localhost:3000"
GOTRUE_JWT_SECRET=""
GOTRUE_DB_MIGRATIONS_PATH=/go/src/github.com/supabase/auth/migrations
GOTRUE_DB_DRIVER=postgres
DATABASE_URL=postgres://supabase_auth_admin:root@postgres:5432/postgres
GOTRUE_API_HOST=0.0.0.0
API_EXTERNAL_URL="http://localhost:9999"
PORT=9999

238
auth_v2.169.0/example.env Normal file
View File

@ -0,0 +1,238 @@
# General Config
# NOTE: The service_role key is required as an authorization header for /admin endpoints
GOTRUE_JWT_SECRET="CHANGE-THIS! VERY IMPORTANT!"
GOTRUE_JWT_EXP="3600"
GOTRUE_JWT_AUD="authenticated"
GOTRUE_JWT_DEFAULT_GROUP_NAME="authenticated"
GOTRUE_JWT_ADMIN_ROLES="supabase_admin,service_role"
# Database & API connection details
GOTRUE_DB_DRIVER="postgres"
DB_NAMESPACE="auth"
DATABASE_URL="postgres://supabase_auth_admin:root@localhost:5432/postgres"
API_EXTERNAL_URL="http://localhost:9999"
GOTRUE_API_HOST="localhost"
PORT="9999"
# SMTP config (generate credentials for signup to work)
GOTRUE_SMTP_HOST=""
GOTRUE_SMTP_PORT=""
GOTRUE_SMTP_USER=""
GOTRUE_SMTP_MAX_FREQUENCY="5s"
GOTRUE_SMTP_PASS=""
GOTRUE_SMTP_ADMIN_EMAIL=""
GOTRUE_SMTP_SENDER_NAME=""
# Mailer config
GOTRUE_MAILER_AUTOCONFIRM="true"
GOTRUE_MAILER_URLPATHS_CONFIRMATION="/verify"
GOTRUE_MAILER_URLPATHS_INVITE="/verify"
GOTRUE_MAILER_URLPATHS_RECOVERY="/verify"
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE="/verify"
GOTRUE_MAILER_SUBJECTS_CONFIRMATION="Confirm Your Email"
GOTRUE_MAILER_SUBJECTS_RECOVERY="Reset Your Password"
GOTRUE_MAILER_SUBJECTS_MAGIC_LINK="Your Magic Link"
GOTRUE_MAILER_SUBJECTS_EMAIL_CHANGE="Confirm Email Change"
GOTRUE_MAILER_SUBJECTS_INVITE="You have been invited"
GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED="true"
# Custom mailer template config
GOTRUE_MAILER_TEMPLATES_INVITE=""
GOTRUE_MAILER_TEMPLATES_CONFIRMATION=""
GOTRUE_MAILER_TEMPLATES_RECOVERY=""
GOTRUE_MAILER_TEMPLATES_MAGIC_LINK=""
GOTRUE_MAILER_TEMPLATES_EMAIL_CHANGE=""
# Signup config
GOTRUE_DISABLE_SIGNUP="false"
GOTRUE_SITE_URL="http://localhost:3000"
GOTRUE_EXTERNAL_EMAIL_ENABLED="true"
GOTRUE_EXTERNAL_PHONE_ENABLED="true"
GOTRUE_EXTERNAL_IOS_BUNDLE_ID="com.supabase.auth"
# Whitelist redirect to URLs here, a comma separated list of URIs (e.g. "https://foo.example.com,https://*.foo.example.com,https://bar.example.com")
GOTRUE_URI_ALLOW_LIST="http://localhost:3000"
# Apple OAuth config
GOTRUE_EXTERNAL_APPLE_ENABLED="false"
GOTRUE_EXTERNAL_APPLE_CLIENT_ID=""
GOTRUE_EXTERNAL_APPLE_SECRET=""
GOTRUE_EXTERNAL_APPLE_REDIRECT_URI="http://localhost:9999/callback"
# Azure OAuth config
GOTRUE_EXTERNAL_AZURE_ENABLED="false"
GOTRUE_EXTERNAL_AZURE_CLIENT_ID=""
GOTRUE_EXTERNAL_AZURE_SECRET=""
GOTRUE_EXTERNAL_AZURE_REDIRECT_URI="https://localhost:9999/callback"
# Bitbucket OAuth config
GOTRUE_EXTERNAL_BITBUCKET_ENABLED="false"
GOTRUE_EXTERNAL_BITBUCKET_CLIENT_ID=""
GOTRUE_EXTERNAL_BITBUCKET_SECRET=""
GOTRUE_EXTERNAL_BITBUCKET_REDIRECT_URI="http://localhost:9999/callback"
# Discord OAuth config
GOTRUE_EXTERNAL_DISCORD_ENABLED="false"
GOTRUE_EXTERNAL_DISCORD_CLIENT_ID=""
GOTRUE_EXTERNAL_DISCORD_SECRET=""
GOTRUE_EXTERNAL_DISCORD_REDIRECT_URI="https://localhost:9999/callback"
# Facebook OAuth config
GOTRUE_EXTERNAL_FACEBOOK_ENABLED="false"
GOTRUE_EXTERNAL_FACEBOOK_CLIENT_ID=""
GOTRUE_EXTERNAL_FACEBOOK_SECRET=""
GOTRUE_EXTERNAL_FACEBOOK_REDIRECT_URI="https://localhost:9999/callback"
# Figma OAuth config
GOTRUE_EXTERNAL_FIGMA_ENABLED="false"
GOTRUE_EXTERNAL_FIGMA_CLIENT_ID=""
GOTRUE_EXTERNAL_FIGMA_SECRET=""
GOTRUE_EXTERNAL_FIGMA_REDIRECT_URI="https://localhost:9999/callback"
# Gitlab OAuth config
GOTRUE_EXTERNAL_GITLAB_ENABLED="false"
GOTRUE_EXTERNAL_GITLAB_CLIENT_ID=""
GOTRUE_EXTERNAL_GITLAB_SECRET=""
GOTRUE_EXTERNAL_GITLAB_REDIRECT_URI="http://localhost:9999/callback"
# Google OAuth config
GOTRUE_EXTERNAL_GOOGLE_ENABLED="false"
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=""
GOTRUE_EXTERNAL_GOOGLE_SECRET=""
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI="http://localhost:9999/callback"
# Github OAuth config
GOTRUE_EXTERNAL_GITHUB_ENABLED="false"
GOTRUE_EXTERNAL_GITHUB_CLIENT_ID=""
GOTRUE_EXTERNAL_GITHUB_SECRET=""
GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI="http://localhost:9999/callback"
# Kakao OAuth config
GOTRUE_EXTERNAL_KAKAO_ENABLED="false"
GOTRUE_EXTERNAL_KAKAO_CLIENT_ID=""
GOTRUE_EXTERNAL_KAKAO_SECRET=""
GOTRUE_EXTERNAL_KAKAO_REDIRECT_URI="http://localhost:9999/callback"
# Notion OAuth config
GOTRUE_EXTERNAL_NOTION_ENABLED="false"
GOTRUE_EXTERNAL_NOTION_CLIENT_ID=""
GOTRUE_EXTERNAL_NOTION_SECRET=""
GOTRUE_EXTERNAL_NOTION_REDIRECT_URI="https://localhost:9999/callback"
# Twitter OAuth1 config
GOTRUE_EXTERNAL_TWITTER_ENABLED="false"
GOTRUE_EXTERNAL_TWITTER_CLIENT_ID=""
GOTRUE_EXTERNAL_TWITTER_SECRET=""
GOTRUE_EXTERNAL_TWITTER_REDIRECT_URI="http://localhost:9999/callback"
# Twitch OAuth config
GOTRUE_EXTERNAL_TWITCH_ENABLED="false"
GOTRUE_EXTERNAL_TWITCH_CLIENT_ID=""
GOTRUE_EXTERNAL_TWITCH_SECRET=""
GOTRUE_EXTERNAL_TWITCH_REDIRECT_URI="http://localhost:9999/callback"
# Spotify OAuth config
GOTRUE_EXTERNAL_SPOTIFY_ENABLED="false"
GOTRUE_EXTERNAL_SPOTIFY_CLIENT_ID=""
GOTRUE_EXTERNAL_SPOTIFY_SECRET=""
GOTRUE_EXTERNAL_SPOTIFY_REDIRECT_URI="http://localhost:9999/callback"
# Keycloak OAuth config
GOTRUE_EXTERNAL_KEYCLOAK_ENABLED="false"
GOTRUE_EXTERNAL_KEYCLOAK_CLIENT_ID=""
GOTRUE_EXTERNAL_KEYCLOAK_SECRET=""
GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI="http://localhost:9999/callback"
GOTRUE_EXTERNAL_KEYCLOAK_URL="https://keycloak.example.com/auth/realms/myrealm"
# Linkedin OAuth config
GOTRUE_EXTERNAL_LINKEDIN_ENABLED="true"
GOTRUE_EXTERNAL_LINKEDIN_CLIENT_ID=""
GOTRUE_EXTERNAL_LINKEDIN_SECRET=""
# Slack OAuth config
GOTRUE_EXTERNAL_SLACK_ENABLED="false"
GOTRUE_EXTERNAL_SLACK_CLIENT_ID=""
GOTRUE_EXTERNAL_SLACK_SECRET=""
GOTRUE_EXTERNAL_SLACK_REDIRECT_URI="http://localhost:9999/callback"
# WorkOS OAuth config
GOTRUE_EXTERNAL_WORKOS_ENABLED="true"
GOTRUE_EXTERNAL_WORKOS_CLIENT_ID=""
GOTRUE_EXTERNAL_WORKOS_SECRET=""
GOTRUE_EXTERNAL_WORKOS_REDIRECT_URI="http://localhost:9999/callback"
# Zoom OAuth config
GOTRUE_EXTERNAL_ZOOM_ENABLED="false"
GOTRUE_EXTERNAL_ZOOM_CLIENT_ID=""
GOTRUE_EXTERNAL_ZOOM_SECRET=""
GOTRUE_EXTERNAL_ZOOM_REDIRECT_URI="http://localhost:9999/callback"
# Anonymous auth config
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED="false"
# PKCE Config
GOTRUE_EXTERNAL_FLOW_STATE_EXPIRY_DURATION="300s"
# Phone provider config
GOTRUE_SMS_AUTOCONFIRM="false"
GOTRUE_SMS_MAX_FREQUENCY="5s"
GOTRUE_SMS_OTP_EXP="6000"
GOTRUE_SMS_OTP_LENGTH="6"
GOTRUE_SMS_PROVIDER="twilio"
GOTRUE_SMS_TWILIO_ACCOUNT_SID=""
GOTRUE_SMS_TWILIO_AUTH_TOKEN=""
GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID=""
GOTRUE_SMS_TEMPLATE="This is from supabase. Your code is {{ .Code }} ."
GOTRUE_SMS_MESSAGEBIRD_ACCESS_KEY=""
GOTRUE_SMS_MESSAGEBIRD_ORIGINATOR=""
GOTRUE_SMS_TEXTLOCAL_API_KEY=""
GOTRUE_SMS_TEXTLOCAL_SENDER=""
GOTRUE_SMS_VONAGE_API_KEY=""
GOTRUE_SMS_VONAGE_API_SECRET=""
GOTRUE_SMS_VONAGE_FROM=""
# Captcha config
GOTRUE_SECURITY_CAPTCHA_ENABLED="false"
GOTRUE_SECURITY_CAPTCHA_PROVIDER="hcaptcha"
GOTRUE_SECURITY_CAPTCHA_SECRET="0x0000000000000000000000000000000000000000"
GOTRUE_SECURITY_CAPTCHA_TIMEOUT="10s"
GOTRUE_SESSION_KEY=""
# SAML config
GOTRUE_EXTERNAL_SAML_ENABLED="true"
GOTRUE_EXTERNAL_SAML_METADATA_URL=""
GOTRUE_EXTERNAL_SAML_API_BASE="http://localhost:9999"
GOTRUE_EXTERNAL_SAML_NAME="auth0"
GOTRUE_EXTERNAL_SAML_SIGNING_CERT=""
GOTRUE_EXTERNAL_SAML_SIGNING_KEY=""
# Additional Security config
GOTRUE_LOG_LEVEL="debug"
GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED="false"
GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL="0"
GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION="false"
GOTRUE_OPERATOR_TOKEN="unused-operator-token"
GOTRUE_RATE_LIMIT_HEADER="X-Forwarded-For"
GOTRUE_RATE_LIMIT_EMAIL_SENT="100"
GOTRUE_MAX_VERIFIED_FACTORS=10
# Auth Hook Configuration
GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED=false
GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI=""
# Only for HTTPS Hooks
GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRET=""
GOTRUE_HOOK_CUSTOM_SMS_PROVIDER_ENABLED=false
GOTRUE_HOOK_CUSTOM_SMS_PROVIDER_URI=""
# Only for HTTPS Hooks
GOTRUE_HOOK_CUSTOM_SMS_PROVIDER_SECRET=""
# Test OTP Config
GOTRUE_SMS_TEST_OTP="<phone-1>:<otp-1>, <phone-2>:<otp-2>..."
GOTRUE_SMS_TEST_OTP_VALID_UNTIL="<ISO date time>" # (e.g. 2023-09-29T08:14:06Z)
GOTRUE_MFA_WEB_AUTHN_ENROLL_ENABLED="false"
GOTRUE_MFA_WEB_AUTHN_VERIFY_ENABLED="false"

163
auth_v2.169.0/go.mod Normal file
View File

@ -0,0 +1,163 @@
module github.com/supabase/auth
require (
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/aaronarduino/goqrsvg v0.0.0-20220419053939-17e843f1dd40
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b
github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc
github.com/coreos/go-oidc/v3 v3.6.0
github.com/didip/tollbooth/v5 v5.1.1
github.com/gobuffalo/validate/v3 v3.3.3 // indirect
github.com/gobwas/glob v0.2.3
github.com/gofrs/uuid v4.3.1+incompatible
github.com/jackc/pgconn v1.14.3
github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jmoiron/sqlx v1.3.5
github.com/joho/godotenv v1.4.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/mitchellh/mapstructure v1.5.0
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450
github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.4.0
github.com/rs/cors v1.11.0
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35
github.com/sethvargo/go-password v0.2.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.31.0
golang.org/x/oauth2 v0.17.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)
require (
github.com/bits-and-blooms/bitset v1.10.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-webauthn/x v0.1.12 // indirect
github.com/gobuffalo/nulls v0.4.2 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/google/go-tpm v0.9.1 // indirect
github.com/jackc/pgx/v4 v4.18.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.5 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
golang.org/x/mod v0.17.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
)
require (
github.com/XSAM/otelsql v0.26.0
github.com/bombsimon/logrusr/v3 v3.0.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.45.0
go.opentelemetry.io/otel v1.26.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0
go.opentelemetry.io/otel/metric v1.26.0
go.opentelemetry.io/otel/sdk v1.26.0
go.opentelemetry.io/otel/sdk/metric v1.26.0
go.opentelemetry.io/otel/trace v1.26.0
gopkg.in/h2non/gock.v1 v1.1.2
)
require (
github.com/bits-and-blooms/bloom/v3 v3.6.0
github.com/crewjam/saml v0.4.14
github.com/deepmap/oapi-codegen v1.12.4
github.com/fatih/structs v1.1.0
github.com/fsnotify/fsnotify v1.7.0
github.com/go-chi/chi/v5 v5.0.12
github.com/go-webauthn/webauthn v0.11.1
github.com/gobuffalo/pop/v6 v6.1.1
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/lestrrat-go/jwx/v2 v2.1.0
github.com/standard-webhooks/standard-webhooks/libraries v0.0.0-20240303152453-e0e82adf1721
github.com/supabase/hibp v0.0.0-20231124125943-d225752ae869
github.com/xeipuuv/gojsonschema v1.2.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.26.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.26.0
go.opentelemetry.io/otel/exporters/prometheus v0.48.0
)
require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/crewjam/httperr v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/gobuffalo/envy v1.10.2 // indirect
github.com/gobuffalo/fizz v1.14.4 // indirect
github.com/gobuffalo/flect v1.0.2 // indirect
github.com/gobuffalo/github_flavored_markdown v1.1.3 // indirect
github.com/gobuffalo/helpers v0.6.7 // indirect
github.com/gobuffalo/plush/v4 v4.1.18 // indirect
github.com/gobuffalo/tags/v3 v3.1.4 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/luna-duclos/instrumentedsql v1.1.3 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.19.0
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/russellhaering/goxmldsig v1.3.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
go.opentelemetry.io/proto/otlp v1.2.0 // indirect
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
golang.org/x/net v0.23.0 // indirect
golang.org/x/sync v0.10.0
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/grpc v1.63.2 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
go 1.22.3

559
auth_v2.169.0/go.sum Normal file
View File

@ -0,0 +1,559 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/XSAM/otelsql v0.26.0 h1:UhAGVBD34Ctbh2aYcm/JAdL+6T6ybrP+YMWYkHqCdmo=
github.com/XSAM/otelsql v0.26.0/go.mod h1:5ciw61eMSh+RtTPN8spvPEPLJpAErZw8mFFPNfYiaxA=
github.com/aaronarduino/goqrsvg v0.0.0-20220419053939-17e843f1dd40 h1:uz4N2yHL4MF8vZX+36n+tcxeUf8D/gL4aJkyouhDw4A=
github.com/aaronarduino/goqrsvg v0.0.0-20220419053939-17e843f1dd40/go.mod h1:dytw+5qs+pdi61fO/S4OmXR7AuEq/HvNCuG03KxQHT4=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62 h1:vMqcPzLT1/mbYew0gM6EJy4/sCNy9lY9rmlFO+pPwhY=
github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62/go.mod h1:r5ZalvRl3tXevRNJkwIB6DC4DD3DMjIlY9NEU1XGoaQ=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88=
github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bloom/v3 v3.6.0 h1:dTU0OVLJSoOhz9m68FTXMFfA39nR8U/nTCs1zb26mOI=
github.com/bits-and-blooms/bloom/v3 v3.6.0/go.mod h1:VKlUSvp0lFIYqxJjzdnSsZEw4iHb1kOL2tfHTgyJBHg=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/bombsimon/logrusr/v3 v3.0.0 h1:tcAoLfuAhKP9npBxWzSdpsvKPQt1XV02nSf2lZA82TQ=
github.com/bombsimon/logrusr/v3 v3.0.0/go.mod h1:PksPPgSFEL2I52pla2glgCyyd2OqOHAnFF5E+g8Ixco=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo=
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/deepmap/oapi-codegen v1.12.4 h1:pPmn6qI9MuOtCz82WY2Xaw46EQjgvxednXXrP7g5Q2s=
github.com/deepmap/oapi-codegen v1.12.4/go.mod h1:3lgHGMu6myQ2vqbbTXH2H1o4eXFTGnFiDaOaKKl5yas=
github.com/didip/tollbooth/v5 v5.1.1 h1:QpKFg56jsbNuQ6FFj++Z1gn2fbBsvAc1ZPLUaDOYW5k=
github.com/didip/tollbooth/v5 v5.1.1/go.mod h1:d9rzwOULswrD3YIrAQmP3bfjxab32Df4IaO6+D25l9g=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-webauthn/webauthn v0.11.1 h1:5G/+dg91/VcaJHTtJUfwIlNJkLwbJCcnUc4W8VtkpzA=
github.com/go-webauthn/webauthn v0.11.1/go.mod h1:YXRm1WG0OtUyDFaVAgB5KG7kVqW+6dYCJ7FTQH4SxEE=
github.com/go-webauthn/x v0.1.12 h1:RjQ5cvApzyU/xLCiP+rub0PE4HBZsLggbxGR5ZpUf/A=
github.com/go-webauthn/x v0.1.12/go.mod h1:XlRcGkNH8PT45TfeJYc6gqpOtiOendHhVmnOxh+5yHs=
github.com/gobuffalo/attrs v1.0.3/go.mod h1:KvDJCE0avbufqS0Bw3UV7RQynESY0jjod+572ctX4t8=
github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4=
github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8=
github.com/gobuffalo/fizz v1.14.4 h1:8uume7joF6niTNWN582IQ2jhGTUoa9g1fiV/tIoGdBs=
github.com/gobuffalo/fizz v1.14.4/go.mod h1:9/2fGNXNeIFOXEEgTPJwiK63e44RjG+Nc4hfMm1ArGM=
github.com/gobuffalo/flect v0.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE=
github.com/gobuffalo/flect v1.0.0/go.mod h1:l9V6xSb4BlXwsxEMj3FVEub2nkdQjWhPvD8XTTlHPQc=
github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA=
github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
github.com/gobuffalo/genny/v2 v2.1.0/go.mod h1:4yoTNk4bYuP3BMM6uQKYPvtP6WsXFGm2w2EFYZdRls8=
github.com/gobuffalo/github_flavored_markdown v1.1.3 h1:rSMPtx9ePkFB22vJ+dH+m/EUBS8doQ3S8LeEXcdwZHk=
github.com/gobuffalo/github_flavored_markdown v1.1.3/go.mod h1:IzgO5xS6hqkDmUh91BW/+Qxo/qYnvfzoz3A7uLkg77I=
github.com/gobuffalo/helpers v0.6.7 h1:C9CedoRSfgWg2ZoIkVXgjI5kgmSpL34Z3qdnzpfNVd8=
github.com/gobuffalo/helpers v0.6.7/go.mod h1:j0u1iC1VqlCaJEEVkZN8Ia3TEzfj/zoXANqyJExTMTA=
github.com/gobuffalo/logger v1.0.7/go.mod h1:u40u6Bq3VVvaMcy5sRBclD8SXhBYPS0Qk95ubt+1xJM=
github.com/gobuffalo/nulls v0.4.2 h1:GAqBR29R3oPY+WCC7JL9KKk9erchaNuV6unsOSZGQkw=
github.com/gobuffalo/nulls v0.4.2/go.mod h1:EElw2zmBYafU2R9W4Ii1ByIj177wA/pc0JdjtD0EsH8=
github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8=
github.com/gobuffalo/plush/v4 v4.1.16/go.mod h1:6t7swVsarJ8qSLw1qyAH/KbrcSTwdun2ASEQkOznakg=
github.com/gobuffalo/plush/v4 v4.1.18 h1:bnPjdMTEUQHqj9TNX2Ck3mxEXYZa+0nrFMNM07kpX9g=
github.com/gobuffalo/plush/v4 v4.1.18/go.mod h1:xi2tJIhFI4UdzIL8sxZtzGYOd2xbBpcFbLZlIPGGZhU=
github.com/gobuffalo/pop/v6 v6.1.1 h1:eUDBaZcb0gYrmFnKwpuTEUA7t5ZHqNfvS4POqJYXDZY=
github.com/gobuffalo/pop/v6 v6.1.1/go.mod h1:1n7jAmI1i7fxuXPZjZb0VBPQDbksRtCoFnrDV5IsvaI=
github.com/gobuffalo/tags/v3 v3.1.4 h1:X/ydLLPhgXV4h04Hp2xlbI2oc5MDaa7eub6zw8oHjsM=
github.com/gobuffalo/tags/v3 v3.1.4/go.mod h1:ArRNo3ErlHO8BtdA0REaZxijuWnWzF6PUXngmMXd2I0=
github.com/gobuffalo/validate/v3 v3.3.3 h1:o7wkIGSvZBYBd6ChQoLxkz2y1pfmhbI4jNJYh6PuNJ4=
github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI=
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 h1:WAvSpGf7MsFuzAtK4Vk7R4EVe+liW4x83r4oWu0WHKw=
github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU=
github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk=
github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.1.0 h1:0zs7Ya6+39qoit7gwAf+cYm1zzgS3fceIdo7RmQ5lkw=
github.com/lestrrat-go/jwx/v2 v2.1.0/go.mod h1:Xpw9QIaUGiIUD1Wx0NcY1sIHwFf8lDuZn/cmxtXYRys=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/luna-duclos/instrumentedsql v1.1.3 h1:t7mvC0z1jUt5A0UQ6I/0H31ryymuQRnJcWCiqV3lSAA=
github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs=
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/patrickmn/go-cache v0.0.0-20170418232947-7ac151875ffb/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russellhaering/goxmldsig v1.3.0 h1:DllIWUgMy0cRUMfGiASiYEa35nsieyD3cigIwLonTPM=
github.com/russellhaering/goxmldsig v1.3.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 h1:eajwn6K3weW5cd1ZXLu2sJ4pvwlBiCWY4uDejOr73gM=
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/standard-webhooks/standard-webhooks/libraries v0.0.0-20240303152453-e0e82adf1721 h1:HTsFo0buahHfjuVUTPDdJRBkfjExkRM1LUBy6crQ7lc=
github.com/standard-webhooks/standard-webhooks/libraries v0.0.0-20240303152453-e0e82adf1721/go.mod h1:L1MQhA6x4dn9r007T033lsaZMv9EmBAdXyU/+EF40fo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/supabase/hibp v0.0.0-20231124125943-d225752ae869 h1:VDuRtwen5Z7QQ5ctuHUse4wAv/JozkKZkdic5vUV4Lg=
github.com/supabase/hibp v0.0.0-20231124125943-d225752ae869/go.mod h1:eHX5nlSMSnyPjUrbYzeqrA8snCe2SKyfizKjU3dkfOw=
github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg=
github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
go.opentelemetry.io/contrib/instrumentation/runtime v0.45.0 h1:2JydY5UiDpqvj2p7sO9bgHuhTy4hgTZ0ymehdq/Ob0Q=
go.opentelemetry.io/contrib/instrumentation/runtime v0.45.0/go.mod h1:ch3a5QxOqVWxas4CzjCFFOOQe+7HgAXC/N1oVxS9DK4=
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.26.0 h1:+hm+I+KigBy3M24/h1p/NHkUx/evbLH0PNcjpMyCHc4=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.26.0/go.mod h1:NjC8142mLvvNT6biDpaMjyz78kyEHIwAJlSX0N9P5KI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.26.0 h1:HGZWGmCVRCVyAs2GQaiHQPbDHo+ObFWeUEOd+zDnp64=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.26.0/go.mod h1:SaH+v38LSCHddyk7RGlU9uZyQoRrKao6IBnJw6Kbn+c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/exporters/prometheus v0.48.0 h1:sBQe3VNGUjY9IKWQC6z2lNqa5iGbDSxhs60ABwK4y0s=
go.opentelemetry.io/otel/exporters/prometheus v0.48.0/go.mod h1:DtrbMzoZWwQHyrQmCfLam5DZbnmorsGbOtTbYHycU5o=
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8=
go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs=
go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y=
go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20161007143504-f4b625ec9b21/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20160926182426-711ca1cb8763/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0=
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=

View File

@ -0,0 +1,21 @@
FAIL=false
for PKG in "crypto"
do
UNCOVERED_FUNCS=$(go tool cover -func=coverage.out | grep "^github.com/supabase/auth/internal/$PKG/" | grep -v '100.0%$')
UNCOVERED_FUNCS_COUNT=$(echo "$UNCOVERED_FUNCS" | wc -l)
if [ "$UNCOVERED_FUNCS_COUNT" -gt 1 ] # wc -l counts +1 line
then
echo "Package $PKG not covered 100% with tests. $UNCOVERED_FUNCS_COUNT functions need more tests. This is mandatory."
echo "$UNCOVERED_FUNCS"
FAIL=true
fi
done
if [ "$FAIL" = "true" ]
then
exit 1
else
exit 0
fi

View File

@ -0,0 +1,15 @@
postgres:
dialect: "postgres"
database: "postgres"
host: {{ envOr "POSTGRES_HOST" "127.0.0.1" }}
port: {{ envOr "POSTGRES_PORT" "5432" }}
user: {{ envOr "POSTGRES_USER" "postgres" }}
password: {{ envOr "POSTGRES_PASSWORD" "root" }}
test:
dialect: "postgres"
database: "postgres"
host: {{ envOr "POSTGRES_HOST" "127.0.0.1" }}
port: {{ envOr "POSTGRES_PORT" "5432" }}
user: {{ envOr "POSTGRES_USER" "postgres" }}
password: {{ envOr "POSTGRES_PASSWORD" "root" }}

View File

@ -0,0 +1,7 @@
CREATE USER supabase_admin LOGIN CREATEROLE CREATEDB REPLICATION BYPASSRLS;
-- Supabase super admin
CREATE USER supabase_auth_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION PASSWORD 'root';
CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION supabase_auth_admin;
GRANT CREATE ON DATABASE postgres TO supabase_auth_admin;
ALTER USER supabase_auth_admin SET search_path = 'auth';

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
DB_ENV=$1
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
DATABASE="$DIR/database.yml"
export GOTRUE_DB_DRIVER="postgres"
export GOTRUE_DB_DATABASE_URL="postgres://supabase_auth_admin:root@localhost:5432/$DB_ENV"
export GOTRUE_DB_MIGRATIONS_PATH=$DIR/../migrations
go run main.go migrate -c $DIR/test.env

View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
docker rm -f gotrue_postgresql >/dev/null 2>/dev/null || true
docker volume inspect postgres_data 2>/dev/null >/dev/null || docker volume create --name postgres_data >/dev/null
docker run --name gotrue_postgresql \
-p 5432:5432 \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=root \
-e POSTGRES_DB=postgres \
--volume postgres_data:/var/lib/postgresql/data \
--volume "$(pwd)"/hack/init_postgres.sql:/docker-entrypoint-initdb.d/init.sql \
-d postgres:15

128
auth_v2.169.0/hack/test.env Normal file
View File

@ -0,0 +1,128 @@
GOTRUE_JWT_SECRET=testsecret
GOTRUE_JWT_EXP=3600
GOTRUE_JWT_AUD="authenticated"
GOTRUE_JWT_ADMIN_ROLES="supabase_admin,service_role"
GOTRUE_JWT_DEFAULT_GROUP_NAME="authenticated"
GOTRUE_DB_DRIVER=postgres
DB_NAMESPACE="auth"
GOTRUE_DB_AUTOMIGRATE=true
DATABASE_URL="postgres://supabase_auth_admin:root@localhost:5432/postgres"
GOTRUE_API_HOST=localhost
PORT=9999
API_EXTERNAL_URL="http://localhost:9999"
GOTRUE_LOG_SQL=none
GOTRUE_LOG_LEVEL=warn
GOTRUE_SITE_URL=https://example.netlify.com
GOTRUE_URI_ALLOW_LIST="http://localhost:3000"
GOTRUE_OPERATOR_TOKEN=foobar
GOTRUE_EXTERNAL_APPLE_ENABLED=true
GOTRUE_EXTERNAL_APPLE_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_APPLE_SECRET=testsecret
GOTRUE_EXTERNAL_APPLE_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_AZURE_ENABLED=true
GOTRUE_EXTERNAL_AZURE_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_AZURE_SECRET=testsecret
GOTRUE_EXTERNAL_AZURE_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_BITBUCKET_ENABLED=true
GOTRUE_EXTERNAL_BITBUCKET_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_BITBUCKET_SECRET=testsecret
GOTRUE_EXTERNAL_BITBUCKET_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_DISCORD_ENABLED=true
GOTRUE_EXTERNAL_DISCORD_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_DISCORD_SECRET=testsecret
GOTRUE_EXTERNAL_DISCORD_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_FACEBOOK_ENABLED=true
GOTRUE_EXTERNAL_FACEBOOK_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_FACEBOOK_SECRET=testsecret
GOTRUE_EXTERNAL_FACEBOOK_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_FLY_ENABLED=true
GOTRUE_EXTERNAL_FLY_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_FLY_SECRET=testsecret
GOTRUE_EXTERNAL_FLY_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_FIGMA_ENABLED=true
GOTRUE_EXTERNAL_FIGMA_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_FIGMA_SECRET=testsecret
GOTRUE_EXTERNAL_FIGMA_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_GITHUB_ENABLED=true
GOTRUE_EXTERNAL_GITHUB_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_GITHUB_SECRET=testsecret
GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_KAKAO_ENABLED=true
GOTRUE_EXTERNAL_KAKAO_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_KAKAO_SECRET=testsecret
GOTRUE_EXTERNAL_KAKAO_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_KEYCLOAK_ENABLED=true
GOTRUE_EXTERNAL_KEYCLOAK_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_KEYCLOAK_SECRET=testsecret
GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_KEYCLOAK_URL=https://keycloak.example.com/auth/realms/myrealm
GOTRUE_EXTERNAL_LINKEDIN_ENABLED=true
GOTRUE_EXTERNAL_LINKEDIN_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_LINKEDIN_SECRET=testsecret
GOTRUE_EXTERNAL_LINKEDIN_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_LINKEDIN_OIDC_ENABLED=true
GOTRUE_EXTERNAL_LINKEDIN_OIDC_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_LINKEDIN_OIDC_SECRET=testsecret
GOTRUE_EXTERNAL_LINKEDIN_OIDC_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_GITLAB_ENABLED=true
GOTRUE_EXTERNAL_GITLAB_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_GITLAB_SECRET=testsecret
GOTRUE_EXTERNAL_GITLAB_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_GOOGLE_ENABLED=true
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_GOOGLE_SECRET=testsecret
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_NOTION_ENABLED=true
GOTRUE_EXTERNAL_NOTION_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_NOTION_SECRET=testsecret
GOTRUE_EXTERNAL_NOTION_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_SPOTIFY_ENABLED=true
GOTRUE_EXTERNAL_SPOTIFY_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_SPOTIFY_SECRET=testsecret
GOTRUE_EXTERNAL_SPOTIFY_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_SLACK_ENABLED=true
GOTRUE_EXTERNAL_SLACK_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_SLACK_SECRET=testsecret
GOTRUE_EXTERNAL_SLACK_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_SLACK_OIDC_ENABLED=true
GOTRUE_EXTERNAL_SLACK_OIDC_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_SLACK_OIDC_SECRET=testsecret
GOTRUE_EXTERNAL_SLACK_OIDC_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_WORKOS_ENABLED=true
GOTRUE_EXTERNAL_WORKOS_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_WORKOS_SECRET=testsecret
GOTRUE_EXTERNAL_WORKOS_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_TWITCH_ENABLED=true
GOTRUE_EXTERNAL_TWITCH_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_TWITCH_SECRET=testsecret
GOTRUE_EXTERNAL_TWITCH_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_TWITTER_ENABLED=true
GOTRUE_EXTERNAL_TWITTER_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_TWITTER_SECRET=testsecret
GOTRUE_EXTERNAL_TWITTER_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_ZOOM_ENABLED=true
GOTRUE_EXTERNAL_ZOOM_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_ZOOM_SECRET=testsecret
GOTRUE_EXTERNAL_ZOOM_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_FLOW_STATE_EXPIRY_DURATION="300s"
GOTRUE_RATE_LIMIT_VERIFY="100000"
GOTRUE_RATE_LIMIT_TOKEN_REFRESH="30"
GOTRUE_RATE_LIMIT_ANONYMOUS_USERS="5"
GOTRUE_RATE_LIMIT_HEADER="My-Custom-Header"
GOTRUE_TRACING_ENABLED=true
GOTRUE_TRACING_EXPORTER=default
GOTRUE_TRACING_HOST=127.0.0.1
GOTRUE_TRACING_PORT=8126
GOTRUE_TRACING_TAGS="env:test"
GOTRUE_SECURITY_CAPTCHA_ENABLED="false"
GOTRUE_SECURITY_CAPTCHA_PROVIDER="hcaptcha"
GOTRUE_SECURITY_CAPTCHA_SECRET="0x0000000000000000000000000000000000000000"
GOTRUE_SECURITY_CAPTCHA_TIMEOUT="10s"
GOTRUE_SAML_ENABLED="true"
GOTRUE_SAML_PRIVATE_KEY="MIIEowIBAAKCAQEAszrVveMQcSsa0Y+zN1ZFb19cRS0jn4UgIHTprW2tVBmO2PABzjY3XFCfx6vPirMAPWBYpsKmXrvm1tr0A6DZYmA8YmJd937VUQ67fa6DMyppBYTjNgGEkEhmKuszvF3MARsIKCGtZqUrmS7UG4404wYxVppnr2EYm3RGtHlkYsXu20MBqSDXP47bQP+PkJqC3BuNGk3xt5UHl2FSFpTHelkI6lBynw16B+lUT1F96SERNDaMqi/TRsZdGe5mB/29ngC/QBMpEbRBLNRir5iUevKS7Pn4aph9Qjaxx/97siktK210FJT23KjHpgcUfjoQ6BgPBTLtEeQdRyDuc/CgfwIDAQABAoIBAGYDWOEpupQPSsZ4mjMnAYJwrp4ZISuMpEqVAORbhspVeb70bLKonT4IDcmiexCg7cQBcLQKGpPVM4CbQ0RFazXZPMVq470ZDeWDEyhoCfk3bGtdxc1Zc9CDxNMs6FeQs6r1beEZug6weG5J/yRn/qYxQife3qEuDMl+lzfl2EN3HYVOSnBmdt50dxRuX26iW3nqqbMRqYn9OHuJ1LvRRfYeyVKqgC5vgt/6Tf7DAJwGe0dD7q08byHV8DBZ0pnMVU0bYpf1GTgMibgjnLjK//EVWafFHtN+RXcjzGmyJrk3+7ZyPUpzpDjO21kpzUQLrpEkkBRnmg6bwHnSrBr8avECgYEA3pq1PTCAOuLQoIm1CWR9/dhkbJQiKTJevlWV8slXQLR50P0WvI2RdFuSxlWmA4xZej8s4e7iD3MYye6SBsQHygOVGc4efvvEZV8/XTlDdyj7iLVGhnEmu2r7AFKzy8cOvXx0QcLg+zNd7vxZv/8D3Qj9Jje2LjLHKM5n/dZ3RzUCgYEAzh5Lo2anc4WN8faLGt7rPkGQF+7/18ImQE11joHWa3LzAEy7FbeOGpE/vhOv5umq5M/KlWFIRahMEQv4RusieHWI19ZLIP+JwQFxWxS+cPp3xOiGcquSAZnlyVSxZ//dlVgaZq2o2MfrxECcovRlaknl2csyf+HjFFwKlNxHm2MCgYAr//R3BdEy0oZeVRndo2lr9YvUEmu2LOihQpWDCd0fQw0ZDA2kc28eysL2RROte95r1XTvq6IvX5a0w11FzRWlDpQ4J4/LlcQ6LVt+98SoFwew+/PWuyLmxLycUbyMOOpm9eSc4wJJZNvaUzMCSkvfMtmm5jgyZYMMQ9A2Ul/9SQKBgB9mfh9mhBwVPIqgBJETZMMXOdxrjI5SBYHGSyJqpT+5Q0vIZLfqPrvNZOiQFzwWXPJ+tV4Mc/YorW3rZOdo6tdvEGnRO6DLTTEaByrY/io3/gcBZXoSqSuVRmxleqFdWWRnB56c1hwwWLqNHU+1671FhL6pNghFYVK4suP6qu4BAoGBAMk+VipXcIlD67mfGrET/xDqiWWBZtgTzTMjTpODhDY1GZck1eb4CQMP5j5V3gFJ4cSgWDJvnWg8rcz0unz/q4aeMGl1rah5WNDWj1QKWMS6vJhMHM/rqN1WHWR0ZnV83svYgtg0zDnQKlLujqW4JmGXLMU7ur6a+e6lpa1fvLsP"
GOTRUE_MAX_VERIFIED_FACTORS=10
GOTRUE_SMS_TEST_OTP_VALID_UNTIL=""
GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPT=true
GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPTION_KEY_ID=abc
GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPTION_KEY=pwFoiPyybQMqNmYVN0gUnpbfpGQV2sDv9vp0ZAxi_Y4
GOTRUE_SECURITY_DB_ENCRYPTION_DECRYPTION_KEYS=abc:pwFoiPyybQMqNmYVN0gUnpbfpGQV2sDv9vp0ZAxi_Y4

View File

@ -0,0 +1,12 @@
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER supabase_admin LOGIN CREATEROLE CREATEDB REPLICATION BYPASSRLS;
-- Supabase super admin
CREATE USER supabase_auth_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION PASSWORD 'root';
CREATE SCHEMA IF NOT EXISTS $DB_NAMESPACE AUTHORIZATION supabase_auth_admin;
GRANT CREATE ON DATABASE postgres TO supabase_auth_admin;
ALTER USER supabase_auth_admin SET search_path = '$DB_NAMESPACE';
EOSQL

View File

@ -0,0 +1,642 @@
package api
import (
"context"
"net/http"
"time"
"github.com/fatih/structs"
"github.com/go-chi/chi/v5"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/sethvargo/go-password/password"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/observability"
"github.com/supabase/auth/internal/storage"
"github.com/supabase/auth/internal/utilities"
"golang.org/x/crypto/bcrypt"
)
type AdminUserParams struct {
Id string `json:"id"`
Aud string `json:"aud"`
Role string `json:"role"`
Email string `json:"email"`
Phone string `json:"phone"`
Password *string `json:"password"`
PasswordHash string `json:"password_hash"`
EmailConfirm bool `json:"email_confirm"`
PhoneConfirm bool `json:"phone_confirm"`
UserMetaData map[string]interface{} `json:"user_metadata"`
AppMetaData map[string]interface{} `json:"app_metadata"`
BanDuration string `json:"ban_duration"`
}
type adminUserDeleteParams struct {
ShouldSoftDelete bool `json:"should_soft_delete"`
}
type adminUserUpdateFactorParams struct {
FriendlyName string `json:"friendly_name"`
Phone string `json:"phone"`
}
type AdminListUsersResponse struct {
Users []*models.User `json:"users"`
Aud string `json:"aud"`
}
func (a *API) loadUser(w http.ResponseWriter, r *http.Request) (context.Context, error) {
ctx := r.Context()
db := a.db.WithContext(ctx)
userID, err := uuid.FromString(chi.URLParam(r, "user_id"))
if err != nil {
return nil, notFoundError(ErrorCodeValidationFailed, "user_id must be an UUID")
}
observability.LogEntrySetField(r, "user_id", userID)
u, err := models.FindUserByID(db, userID)
if err != nil {
if models.IsNotFoundError(err) {
return nil, notFoundError(ErrorCodeUserNotFound, "User not found")
}
return nil, internalServerError("Database error loading user").WithInternalError(err)
}
return withUser(ctx, u), nil
}
// Use only after requireAuthentication, so that there is a valid user
func (a *API) loadFactor(w http.ResponseWriter, r *http.Request) (context.Context, error) {
ctx := r.Context()
db := a.db.WithContext(ctx)
user := getUser(ctx)
factorID, err := uuid.FromString(chi.URLParam(r, "factor_id"))
if err != nil {
return nil, notFoundError(ErrorCodeValidationFailed, "factor_id must be an UUID")
}
observability.LogEntrySetField(r, "factor_id", factorID)
factor, err := user.FindOwnedFactorByID(db, factorID)
if err != nil {
if models.IsNotFoundError(err) {
return nil, notFoundError(ErrorCodeMFAFactorNotFound, "Factor not found")
}
return nil, internalServerError("Database error loading factor").WithInternalError(err)
}
return withFactor(ctx, factor), nil
}
func (a *API) getAdminParams(r *http.Request) (*AdminUserParams, error) {
params := &AdminUserParams{}
if err := retrieveRequestParams(r, params); err != nil {
return nil, err
}
return params, nil
}
// adminUsers responds with a list of all users in a given audience
func (a *API) adminUsers(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)
aud := a.requestAud(ctx, r)
pageParams, err := paginate(r)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "Bad Pagination Parameters: %v", err).WithInternalError(err)
}
sortParams, err := sort(r, map[string]bool{models.CreatedAt: true}, []models.SortField{{Name: models.CreatedAt, Dir: models.Descending}})
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "Bad Sort Parameters: %v", err)
}
filter := r.URL.Query().Get("filter")
users, err := models.FindUsersInAudience(db, aud, pageParams, sortParams, filter)
if err != nil {
return internalServerError("Database error finding users").WithInternalError(err)
}
addPaginationHeaders(w, r, pageParams)
return sendJSON(w, http.StatusOK, AdminListUsersResponse{
Users: users,
Aud: aud,
})
}
// adminUserGet returns information about a single user
func (a *API) adminUserGet(w http.ResponseWriter, r *http.Request) error {
user := getUser(r.Context())
return sendJSON(w, http.StatusOK, user)
}
// adminUserUpdate updates a single user object
func (a *API) adminUserUpdate(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)
config := a.config
user := getUser(ctx)
adminUser := getAdminUser(ctx)
params, err := a.getAdminParams(r)
if err != nil {
return err
}
if params.Email != "" {
params.Email, err = a.validateEmail(params.Email)
if err != nil {
return err
}
}
if params.Phone != "" {
params.Phone, err = validatePhone(params.Phone)
if err != nil {
return err
}
}
var banDuration *time.Duration
if params.BanDuration != "" {
duration := time.Duration(0)
if params.BanDuration != "none" {
duration, err = time.ParseDuration(params.BanDuration)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "invalid format for ban duration: %v", err)
}
}
banDuration = &duration
}
if params.Password != nil {
password := *params.Password
if err := a.checkPasswordStrength(ctx, password); err != nil {
return err
}
if err := user.SetPassword(ctx, password, config.Security.DBEncryption.Encrypt, config.Security.DBEncryption.EncryptionKeyID, config.Security.DBEncryption.EncryptionKey); err != nil {
return err
}
}
err = db.Transaction(func(tx *storage.Connection) error {
if params.Role != "" {
if terr := user.SetRole(tx, params.Role); terr != nil {
return terr
}
}
if params.EmailConfirm {
if terr := user.Confirm(tx); terr != nil {
return terr
}
}
if params.PhoneConfirm {
if terr := user.ConfirmPhone(tx); terr != nil {
return terr
}
}
if params.Password != nil {
if terr := user.UpdatePassword(tx, nil); terr != nil {
return terr
}
}
var identities []models.Identity
if params.Email != "" {
if identity, terr := models.FindIdentityByIdAndProvider(tx, user.ID.String(), "email"); terr != nil && !models.IsNotFoundError(terr) {
return terr
} else if identity == nil {
// if the user doesn't have an existing email
// then updating the user's email should create a new email identity
i, terr := a.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{
Subject: user.ID.String(),
Email: params.Email,
EmailVerified: params.EmailConfirm,
}))
if terr != nil {
return terr
}
identities = append(identities, *i)
} else {
// update the existing email identity
if terr := identity.UpdateIdentityData(tx, map[string]interface{}{
"email": params.Email,
"email_verified": params.EmailConfirm,
}); terr != nil {
return terr
}
}
if user.IsAnonymous && params.EmailConfirm {
user.IsAnonymous = false
if terr := tx.UpdateOnly(user, "is_anonymous"); terr != nil {
return terr
}
}
if terr := user.SetEmail(tx, params.Email); terr != nil {
return terr
}
}
if params.Phone != "" {
if identity, terr := models.FindIdentityByIdAndProvider(tx, user.ID.String(), "phone"); terr != nil && !models.IsNotFoundError(terr) {
return terr
} else if identity == nil {
// if the user doesn't have an existing phone
// then updating the user's phone should create a new phone identity
identity, terr := a.createNewIdentity(tx, user, "phone", structs.Map(provider.Claims{
Subject: user.ID.String(),
Phone: params.Phone,
PhoneVerified: params.PhoneConfirm,
}))
if terr != nil {
return terr
}
identities = append(identities, *identity)
} else {
// update the existing phone identity
if terr := identity.UpdateIdentityData(tx, map[string]interface{}{
"phone": params.Phone,
"phone_verified": params.PhoneConfirm,
}); terr != nil {
return terr
}
}
if user.IsAnonymous && params.PhoneConfirm {
user.IsAnonymous = false
if terr := tx.UpdateOnly(user, "is_anonymous"); terr != nil {
return terr
}
}
if terr := user.SetPhone(tx, params.Phone); terr != nil {
return terr
}
}
user.Identities = append(user.Identities, identities...)
if params.AppMetaData != nil {
if terr := user.UpdateAppMetaData(tx, params.AppMetaData); terr != nil {
return terr
}
}
if params.UserMetaData != nil {
if terr := user.UpdateUserMetaData(tx, params.UserMetaData); terr != nil {
return terr
}
}
if banDuration != nil {
if terr := user.Ban(tx, *banDuration); terr != nil {
return terr
}
}
if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UserModifiedAction, "", map[string]interface{}{
"user_id": user.ID,
"user_email": user.Email,
"user_phone": user.Phone,
}); terr != nil {
return terr
}
return nil
})
if err != nil {
return internalServerError("Error updating user").WithInternalError(err)
}
return sendJSON(w, http.StatusOK, user)
}
// adminUserCreate creates a new user based on the provided data
func (a *API) adminUserCreate(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)
config := a.config
adminUser := getAdminUser(ctx)
params, err := a.getAdminParams(r)
if err != nil {
return err
}
aud := a.requestAud(ctx, r)
if params.Aud != "" {
aud = params.Aud
}
if params.Email == "" && params.Phone == "" {
return badRequestError(ErrorCodeValidationFailed, "Cannot create a user without either an email or phone")
}
var providers []string
if params.Email != "" {
params.Email, err = a.validateEmail(params.Email)
if err != nil {
return err
}
if user, err := models.IsDuplicatedEmail(db, params.Email, aud, nil); err != nil {
return internalServerError("Database error checking email").WithInternalError(err)
} else if user != nil {
return unprocessableEntityError(ErrorCodeEmailExists, DuplicateEmailMsg)
}
providers = append(providers, "email")
}
if params.Phone != "" {
params.Phone, err = validatePhone(params.Phone)
if err != nil {
return err
}
if exists, err := models.IsDuplicatedPhone(db, params.Phone, aud); err != nil {
return internalServerError("Database error checking phone").WithInternalError(err)
} else if exists {
return unprocessableEntityError(ErrorCodePhoneExists, "Phone number already registered by another user")
}
providers = append(providers, "phone")
}
if params.Password != nil && params.PasswordHash != "" {
return badRequestError(ErrorCodeValidationFailed, "Only a password or a password hash should be provided")
}
if (params.Password == nil || *params.Password == "") && params.PasswordHash == "" {
password, err := password.Generate(64, 10, 0, false, true)
if err != nil {
return internalServerError("Error generating password").WithInternalError(err)
}
params.Password = &password
}
var user *models.User
if params.PasswordHash != "" {
user, err = models.NewUserWithPasswordHash(params.Phone, params.Email, params.PasswordHash, aud, params.UserMetaData)
} else {
user, err = models.NewUser(params.Phone, params.Email, *params.Password, aud, params.UserMetaData)
}
if err != nil {
if errors.Is(err, bcrypt.ErrPasswordTooLong) {
return badRequestError(ErrorCodeValidationFailed, err.Error())
}
return internalServerError("Error creating user").WithInternalError(err)
}
if params.Id != "" {
customId, err := uuid.FromString(params.Id)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "ID must conform to the uuid v4 format")
}
if customId == uuid.Nil {
return badRequestError(ErrorCodeValidationFailed, "ID cannot be a nil uuid")
}
user.ID = customId
}
user.AppMetaData = map[string]interface{}{
// TODO: Deprecate "provider" field
// default to the first provider in the providers slice
"provider": providers[0],
"providers": providers,
}
var banDuration *time.Duration
if params.BanDuration != "" {
duration := time.Duration(0)
if params.BanDuration != "none" {
duration, err = time.ParseDuration(params.BanDuration)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "invalid format for ban duration: %v", err)
}
}
banDuration = &duration
}
err = db.Transaction(func(tx *storage.Connection) error {
if terr := tx.Create(user); terr != nil {
return terr
}
var identities []models.Identity
if user.GetEmail() != "" {
identity, terr := a.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{
Subject: user.ID.String(),
Email: user.GetEmail(),
}))
if terr != nil {
return terr
}
identities = append(identities, *identity)
}
if user.GetPhone() != "" {
identity, terr := a.createNewIdentity(tx, user, "phone", structs.Map(provider.Claims{
Subject: user.ID.String(),
Phone: user.GetPhone(),
}))
if terr != nil {
return terr
}
identities = append(identities, *identity)
}
user.Identities = identities
if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UserSignedUpAction, "", map[string]interface{}{
"user_id": user.ID,
"user_email": user.Email,
"user_phone": user.Phone,
}); terr != nil {
return terr
}
role := config.JWT.DefaultGroupName
if params.Role != "" {
role = params.Role
}
if terr := user.SetRole(tx, role); terr != nil {
return terr
}
if params.AppMetaData != nil {
if terr := user.UpdateAppMetaData(tx, params.AppMetaData); terr != nil {
return terr
}
}
if params.EmailConfirm {
if terr := user.Confirm(tx); terr != nil {
return terr
}
}
if params.PhoneConfirm {
if terr := user.ConfirmPhone(tx); terr != nil {
return terr
}
}
if banDuration != nil {
if terr := user.Ban(tx, *banDuration); terr != nil {
return terr
}
}
return nil
})
if err != nil {
return internalServerError("Database error creating new user").WithInternalError(err)
}
return sendJSON(w, http.StatusOK, user)
}
// adminUserDelete deletes a user
func (a *API) adminUserDelete(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
user := getUser(ctx)
adminUser := getAdminUser(ctx)
// ShouldSoftDelete defaults to false
params := &adminUserDeleteParams{}
if body, _ := utilities.GetBodyBytes(r); len(body) != 0 {
// we only want to parse the body if it's not empty
// retrieveRequestParams will handle any errors with stream
if err := retrieveRequestParams(r, params); err != nil {
return err
}
}
err := a.db.Transaction(func(tx *storage.Connection) error {
if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UserDeletedAction, "", map[string]interface{}{
"user_id": user.ID,
"user_email": user.Email,
"user_phone": user.Phone,
}); terr != nil {
return internalServerError("Error recording audit log entry").WithInternalError(terr)
}
if params.ShouldSoftDelete {
if user.DeletedAt != nil {
// user has been soft deleted already
return nil
}
if terr := user.SoftDeleteUser(tx); terr != nil {
return internalServerError("Error soft deleting user").WithInternalError(terr)
}
if terr := user.SoftDeleteUserIdentities(tx); terr != nil {
return internalServerError("Error soft deleting user identities").WithInternalError(terr)
}
// hard delete all associated factors
if terr := models.DeleteFactorsByUserId(tx, user.ID); terr != nil {
return internalServerError("Error deleting user's factors").WithInternalError(terr)
}
// hard delete all associated sessions
if terr := models.Logout(tx, user.ID); terr != nil {
return internalServerError("Error deleting user's sessions").WithInternalError(terr)
}
} else {
if terr := tx.Destroy(user); terr != nil {
return internalServerError("Database error deleting user").WithInternalError(terr)
}
}
return nil
})
if err != nil {
return err
}
return sendJSON(w, http.StatusOK, map[string]interface{}{})
}
func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
user := getUser(ctx)
factor := getFactor(ctx)
err := a.db.Transaction(func(tx *storage.Connection) error {
if terr := models.NewAuditLogEntry(r, tx, user, models.DeleteFactorAction, r.RemoteAddr, map[string]interface{}{
"user_id": user.ID,
"factor_id": factor.ID,
}); terr != nil {
return terr
}
if terr := tx.Destroy(factor); terr != nil {
return internalServerError("Database error deleting factor").WithInternalError(terr)
}
return nil
})
if err != nil {
return err
}
return sendJSON(w, http.StatusOK, factor)
}
func (a *API) adminUserGetFactors(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
user := getUser(ctx)
return sendJSON(w, http.StatusOK, user.Factors)
}
// adminUserUpdate updates a single factor object
func (a *API) adminUserUpdateFactor(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
factor := getFactor(ctx)
user := getUser(ctx)
adminUser := getAdminUser(ctx)
params := &adminUserUpdateFactorParams{}
if err := retrieveRequestParams(r, params); err != nil {
return err
}
err := a.db.Transaction(func(tx *storage.Connection) error {
if params.FriendlyName != "" {
if terr := factor.UpdateFriendlyName(tx, params.FriendlyName); terr != nil {
return terr
}
}
if params.Phone != "" && factor.IsPhoneFactor() {
phone, err := validatePhone(params.Phone)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "Invalid phone number format (E.164 required)")
}
if terr := factor.UpdatePhone(tx, phone); terr != nil {
return terr
}
}
if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UpdateFactorAction, "", map[string]interface{}{
"user_id": user.ID,
"factor_id": factor.ID,
"factor_type": factor.FactorType,
}); terr != nil {
return terr
}
return nil
})
if err != nil {
return err
}
return sendJSON(w, http.StatusOK, factor)
}

View File

@ -0,0 +1,915 @@
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gofrs/uuid"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/models"
)
type AdminTestSuite struct {
suite.Suite
User *models.User
API *API
Config *conf.GlobalConfiguration
token string
}
func TestAdmin(t *testing.T) {
api, config, err := setupAPIForTest()
require.NoError(t, err)
ts := &AdminTestSuite{
API: api,
Config: config,
}
defer api.db.Close()
suite.Run(t, ts)
}
func (ts *AdminTestSuite) SetupTest() {
models.TruncateAll(ts.API.db)
ts.Config.External.Email.Enabled = true
claims := &AccessTokenClaims{
Role: "supabase_admin",
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(ts.Config.JWT.Secret))
require.NoError(ts.T(), err, "Error generating admin jwt")
ts.token = token
}
// TestAdminUsersUnauthorized tests API /admin/users route without authentication
func (ts *AdminTestSuite) TestAdminUsersUnauthorized() {
req := httptest.NewRequest(http.MethodGet, "/admin/users", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusUnauthorized, w.Code)
}
// TestAdminUsers tests API /admin/users route
func (ts *AdminTestSuite) TestAdminUsers() {
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/admin/users", nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
assert.Equal(ts.T(), "</admin/users?page=0>; rel=\"last\"", w.Header().Get("Link"))
assert.Equal(ts.T(), "0", w.Header().Get("X-Total-Count"))
}
// TestAdminUsers tests API /admin/users route
func (ts *AdminTestSuite) TestAdminUsers_Pagination() {
u, err := models.NewUser("12345678", "test1@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
u, err = models.NewUser("987654321", "test2@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/admin/users?per_page=1", nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
assert.Equal(ts.T(), "</admin/users?page=2&per_page=1>; rel=\"next\", </admin/users?page=2&per_page=1>; rel=\"last\"", w.Header().Get("Link"))
assert.Equal(ts.T(), "2", w.Header().Get("X-Total-Count"))
data := make(map[string]interface{})
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
for _, user := range data["users"].([]interface{}) {
assert.NotEmpty(ts.T(), user)
}
}
// TestAdminUsers tests API /admin/users route
func (ts *AdminTestSuite) TestAdminUsers_SortAsc() {
u, err := models.NewUser("", "test1@example.com", "test", ts.Config.JWT.Aud, nil)
u.CreatedAt = time.Now().Add(-time.Minute)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
u, err = models.NewUser("", "test2@example.com", "test", ts.Config.JWT.Aud, nil)
u.CreatedAt = time.Now()
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/admin/users", nil)
qv := req.URL.Query()
qv.Set("sort", "created_at asc")
req.URL.RawQuery = qv.Encode()
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
data := struct {
Users []*models.User `json:"users"`
Aud string `json:"aud"`
}{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
require.Len(ts.T(), data.Users, 2)
assert.Equal(ts.T(), "test1@example.com", data.Users[0].GetEmail())
assert.Equal(ts.T(), "test2@example.com", data.Users[1].GetEmail())
}
// TestAdminUsers tests API /admin/users route
func (ts *AdminTestSuite) TestAdminUsers_SortDesc() {
u, err := models.NewUser("12345678", "test1@example.com", "test", ts.Config.JWT.Aud, nil)
u.CreatedAt = time.Now().Add(-time.Minute)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
u, err = models.NewUser("987654321", "test2@example.com", "test", ts.Config.JWT.Aud, nil)
u.CreatedAt = time.Now()
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/admin/users", nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
data := struct {
Users []*models.User `json:"users"`
Aud string `json:"aud"`
}{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
require.Len(ts.T(), data.Users, 2)
assert.Equal(ts.T(), "test2@example.com", data.Users[0].GetEmail())
assert.Equal(ts.T(), "test1@example.com", data.Users[1].GetEmail())
}
// TestAdminUsers tests API /admin/users route
func (ts *AdminTestSuite) TestAdminUsers_FilterEmail() {
u, err := models.NewUser("", "test1@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/admin/users?filter=test1", nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
data := struct {
Users []*models.User `json:"users"`
Aud string `json:"aud"`
}{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
require.Len(ts.T(), data.Users, 1)
assert.Equal(ts.T(), "test1@example.com", data.Users[0].GetEmail())
}
// TestAdminUsers tests API /admin/users route
func (ts *AdminTestSuite) TestAdminUsers_FilterName() {
u, err := models.NewUser("", "test1@example.com", "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test User"})
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
u, err = models.NewUser("", "test2@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/admin/users?filter=User", nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
data := struct {
Users []*models.User `json:"users"`
Aud string `json:"aud"`
}{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
require.Len(ts.T(), data.Users, 1)
assert.Equal(ts.T(), "test1@example.com", data.Users[0].GetEmail())
}
// TestAdminUserCreate tests API /admin/user route (POST)
func (ts *AdminTestSuite) TestAdminUserCreate() {
cases := []struct {
desc string
params map[string]interface{}
expected map[string]interface{}
}{
{
desc: "Only phone",
params: map[string]interface{}{
"phone": "123456789",
"password": "test1",
},
expected: map[string]interface{}{
"email": "",
"phone": "123456789",
"isAuthenticated": true,
"provider": "phone",
"providers": []string{"phone"},
"password": "test1",
},
},
{
desc: "With password",
params: map[string]interface{}{
"email": "test1@example.com",
"phone": "123456789",
"password": "test1",
},
expected: map[string]interface{}{
"email": "test1@example.com",
"phone": "123456789",
"isAuthenticated": true,
"provider": "email",
"providers": []string{"email", "phone"},
"password": "test1",
},
},
{
desc: "Without password",
params: map[string]interface{}{
"email": "test2@example.com",
"phone": "",
},
expected: map[string]interface{}{
"email": "test2@example.com",
"phone": "",
"isAuthenticated": false,
"provider": "email",
"providers": []string{"email"},
},
},
{
desc: "With empty string password",
params: map[string]interface{}{
"email": "test3@example.com",
"phone": "",
"password": "",
},
expected: map[string]interface{}{
"email": "test3@example.com",
"phone": "",
"isAuthenticated": false,
"provider": "email",
"providers": []string{"email"},
"password": "",
},
},
{
desc: "Ban created user",
params: map[string]interface{}{
"email": "test4@example.com",
"phone": "",
"password": "test1",
"ban_duration": "24h",
},
expected: map[string]interface{}{
"email": "test4@example.com",
"phone": "",
"isAuthenticated": true,
"provider": "email",
"providers": []string{"email"},
"password": "test1",
},
},
{
desc: "With password hash",
params: map[string]interface{}{
"email": "test5@example.com",
"password_hash": "$2y$10$SXEz2HeT8PUIGQXo9yeUIem8KzNxgG0d7o/.eGj2rj8KbRgAuRVlq",
},
expected: map[string]interface{}{
"email": "test5@example.com",
"phone": "",
"isAuthenticated": true,
"provider": "email",
"providers": []string{"email"},
"password": "test",
},
},
{
desc: "With custom id",
params: map[string]interface{}{
"id": "fc56ab41-2010-4870-a9b9-767c1dc573fb",
"email": "test6@example.com",
"password": "test",
},
expected: map[string]interface{}{
"id": "fc56ab41-2010-4870-a9b9-767c1dc573fb",
"email": "test6@example.com",
"phone": "",
"isAuthenticated": true,
"provider": "email",
"providers": []string{"email"},
"password": "test",
},
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.params))
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/admin/users", &buffer)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.Config.External.Phone.Enabled = true
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
data := models.User{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
assert.Equal(ts.T(), c.expected["email"], data.GetEmail())
assert.Equal(ts.T(), c.expected["phone"], data.GetPhone())
assert.Equal(ts.T(), c.expected["provider"], data.AppMetaData["provider"])
assert.ElementsMatch(ts.T(), c.expected["providers"], data.AppMetaData["providers"])
u, err := models.FindUserByID(ts.API.db, data.ID)
require.NoError(ts.T(), err)
// verify that the corresponding identities were created
require.NotEmpty(ts.T(), u.Identities)
for _, identity := range u.Identities {
require.Equal(ts.T(), u.ID, identity.UserID)
if identity.Provider == "email" {
require.Equal(ts.T(), c.expected["email"], identity.IdentityData["email"])
}
if identity.Provider == "phone" {
require.Equal(ts.T(), c.expected["phone"], identity.IdentityData["phone"])
}
}
if _, ok := c.expected["password"]; ok {
expectedPassword := fmt.Sprintf("%v", c.expected["password"])
isAuthenticated, _, err := u.Authenticate(context.Background(), ts.API.db, expectedPassword, ts.API.config.Security.DBEncryption.DecryptionKeys, ts.API.config.Security.DBEncryption.Encrypt, ts.API.config.Security.DBEncryption.EncryptionKeyID)
require.NoError(ts.T(), err)
require.Equal(ts.T(), c.expected["isAuthenticated"], isAuthenticated)
}
if id, ok := c.expected["id"]; ok {
uid, err := uuid.FromString(id.(string))
require.NoError(ts.T(), err)
require.Equal(ts.T(), uid, data.ID)
}
// remove created user after each case
require.NoError(ts.T(), ts.API.db.Destroy(u))
})
}
}
// TestAdminUserGet tests API /admin/user route (GET)
func (ts *AdminTestSuite) TestAdminUserGet() {
u, err := models.NewUser("12345678", "test1@example.com", "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test Get User"})
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/admin/users/%s", u.ID), nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
data := make(map[string]interface{})
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
assert.Equal(ts.T(), data["email"], "test1@example.com")
assert.NotNil(ts.T(), data["app_metadata"])
assert.NotNil(ts.T(), data["user_metadata"])
md := data["user_metadata"].(map[string]interface{})
assert.Len(ts.T(), md, 1)
assert.Equal(ts.T(), "Test Get User", md["full_name"])
}
// TestAdminUserUpdate tests API /admin/user route (UPDATE)
func (ts *AdminTestSuite) TestAdminUserUpdate() {
u, err := models.NewUser("12345678", "test1@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
var buffer bytes.Buffer
newEmail := "test2@example.com"
newPhone := "234567890"
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"role": "testing",
"app_metadata": map[string]interface{}{
"roles": []string{"writer", "editor"},
},
"user_metadata": map[string]interface{}{
"name": "David",
},
"ban_duration": "24h",
"email": newEmail,
"phone": newPhone,
}))
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/admin/users/%s", u.ID), &buffer)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
data := models.User{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
assert.Equal(ts.T(), "testing", data.Role)
assert.NotNil(ts.T(), data.UserMetaData)
assert.Equal(ts.T(), "David", data.UserMetaData["name"])
assert.Equal(ts.T(), newEmail, data.GetEmail())
assert.Equal(ts.T(), newPhone, data.GetPhone())
assert.NotNil(ts.T(), data.AppMetaData)
assert.Len(ts.T(), data.AppMetaData["roles"], 2)
assert.Contains(ts.T(), data.AppMetaData["roles"], "writer")
assert.Contains(ts.T(), data.AppMetaData["roles"], "editor")
assert.NotNil(ts.T(), data.BannedUntil)
u, err = models.FindUserByID(ts.API.db, data.ID)
require.NoError(ts.T(), err)
// check if the corresponding identities were successfully created
require.NotEmpty(ts.T(), u.Identities)
for _, identity := range u.Identities {
// for email & phone identities, the providerId is the same as the userId
require.Equal(ts.T(), u.ID.String(), identity.ProviderID)
require.Equal(ts.T(), u.ID, identity.UserID)
if identity.Provider == "email" {
require.Equal(ts.T(), newEmail, identity.IdentityData["email"])
}
if identity.Provider == "phone" {
require.Equal(ts.T(), newPhone, identity.IdentityData["phone"])
}
}
}
func (ts *AdminTestSuite) TestAdminUserUpdatePasswordFailed() {
u, err := models.NewUser("12345678", "test1@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
var updateEndpoint = fmt.Sprintf("/admin/users/%s", u.ID)
ts.Config.Password.MinLength = 6
ts.Run("Password doesn't meet minimum length", func() {
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"password": "",
}))
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, updateEndpoint, &buffer)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusUnprocessableEntity, w.Code)
})
}
func (ts *AdminTestSuite) TestAdminUserUpdateBannedUntilFailed() {
u, err := models.NewUser("", "test1@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
var updateEndpoint = fmt.Sprintf("/admin/users/%s", u.ID)
ts.Config.Password.MinLength = 6
ts.Run("Incorrect format for ban_duration", func() {
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"ban_duration": "24",
}))
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, updateEndpoint, &buffer)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusBadRequest, w.Code)
})
}
// TestAdminUserDelete tests API /admin/users route (DELETE)
func (ts *AdminTestSuite) TestAdminUserDelete() {
type expected struct {
code int
err error
}
signupParams := &SignupParams{
Email: "test-delete@example.com",
Password: "test",
Data: map[string]interface{}{"name": "test"},
Provider: "email",
Aud: ts.Config.JWT.Aud,
}
cases := []struct {
desc string
body map[string]interface{}
isSoftDelete string
isSSOUser bool
expected expected
}{
{
desc: "Test admin delete user (default)",
isSoftDelete: "",
isSSOUser: false,
expected: expected{code: http.StatusOK, err: models.UserNotFoundError{}},
body: nil,
},
{
desc: "Test admin delete user (hard deletion)",
isSoftDelete: "?is_soft_delete=false",
isSSOUser: false,
expected: expected{code: http.StatusOK, err: models.UserNotFoundError{}},
body: map[string]interface{}{
"should_soft_delete": false,
},
},
{
desc: "Test admin delete user (soft deletion)",
isSoftDelete: "?is_soft_delete=true",
isSSOUser: false,
expected: expected{code: http.StatusOK, err: models.UserNotFoundError{}},
body: map[string]interface{}{
"should_soft_delete": true,
},
},
{
desc: "Test admin delete user (soft deletion & sso user)",
isSoftDelete: "?is_soft_delete=true",
isSSOUser: true,
expected: expected{code: http.StatusOK, err: nil},
body: map[string]interface{}{
"should_soft_delete": true,
},
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.body))
u, err := signupParams.ToUserModel(false /* <- isSSOUser */)
require.NoError(ts.T(), err)
u, err = ts.API.signupNewUser(ts.API.db, u)
require.NoError(ts.T(), err)
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s", u.ID), &buffer)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), c.expected.code, w.Code)
if c.isSSOUser {
u, err = models.FindUserByID(ts.API.db, u.ID)
require.NotNil(ts.T(), u)
} else {
_, err = models.FindUserByEmailAndAudience(ts.API.db, signupParams.Email, ts.Config.JWT.Aud)
}
require.Equal(ts.T(), c.expected.err, err)
})
}
}
func (ts *AdminTestSuite) TestAdminUserSoftDeletion() {
// create user
u, err := models.NewUser("123456789", "test@example.com", "secret", ts.Config.JWT.Aud, map[string]interface{}{"name": "test"})
require.NoError(ts.T(), err)
u.ConfirmationToken = "some_token"
u.RecoveryToken = "some_token"
u.EmailChangeTokenCurrent = "some_token"
u.EmailChangeTokenNew = "some_token"
u.PhoneChangeToken = "some_token"
u.AppMetaData = map[string]interface{}{
"provider": "email",
}
require.NoError(ts.T(), ts.API.db.Create(u))
require.NoError(ts.T(), models.CreateOneTimeToken(ts.API.db, u.ID, u.GetEmail(), u.ConfirmationToken, models.ConfirmationToken))
require.NoError(ts.T(), models.CreateOneTimeToken(ts.API.db, u.ID, u.GetEmail(), u.RecoveryToken, models.RecoveryToken))
require.NoError(ts.T(), models.CreateOneTimeToken(ts.API.db, u.ID, u.GetEmail(), u.EmailChangeTokenCurrent, models.EmailChangeTokenCurrent))
require.NoError(ts.T(), models.CreateOneTimeToken(ts.API.db, u.ID, u.GetEmail(), u.EmailChangeTokenNew, models.EmailChangeTokenNew))
require.NoError(ts.T(), models.CreateOneTimeToken(ts.API.db, u.ID, u.GetPhone(), u.PhoneChangeToken, models.PhoneChangeToken))
// create user identities
_, err = ts.API.createNewIdentity(ts.API.db, u, "email", map[string]interface{}{
"sub": "123456",
"email": "test@example.com",
})
require.NoError(ts.T(), err)
_, err = ts.API.createNewIdentity(ts.API.db, u, "github", map[string]interface{}{
"sub": "234567",
"email": "test@example.com",
})
require.NoError(ts.T(), err)
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"should_soft_delete": true,
}))
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s", u.ID), &buffer)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
// get soft-deleted user from db
deletedUser, err := models.FindUserByID(ts.API.db, u.ID)
require.NoError(ts.T(), err)
require.Empty(ts.T(), deletedUser.ConfirmationToken)
require.Empty(ts.T(), deletedUser.RecoveryToken)
require.Empty(ts.T(), deletedUser.EmailChangeTokenCurrent)
require.Empty(ts.T(), deletedUser.EmailChangeTokenNew)
require.Empty(ts.T(), deletedUser.EncryptedPassword)
require.Empty(ts.T(), deletedUser.PhoneChangeToken)
require.Empty(ts.T(), deletedUser.UserMetaData)
require.Empty(ts.T(), deletedUser.AppMetaData)
require.NotEmpty(ts.T(), deletedUser.DeletedAt)
require.NotEmpty(ts.T(), deletedUser.GetEmail())
// get soft-deleted user's identity from db
deletedIdentities, err := models.FindIdentitiesByUserID(ts.API.db, deletedUser.ID)
require.NoError(ts.T(), err)
for _, identity := range deletedIdentities {
require.Empty(ts.T(), identity.IdentityData)
}
}
func (ts *AdminTestSuite) TestAdminUserCreateWithDisabledLogin() {
var cases = []struct {
desc string
customConfig *conf.GlobalConfiguration
userData map[string]interface{}
expected int
}{
{
desc: "Email Signups Disabled",
customConfig: &conf.GlobalConfiguration{
JWT: ts.Config.JWT,
External: conf.ProviderConfiguration{
Email: conf.EmailProviderConfiguration{
Enabled: false,
},
},
},
userData: map[string]interface{}{
"email": "test1@example.com",
"password": "test1",
},
expected: http.StatusOK,
},
{
desc: "Phone Signups Disabled",
customConfig: &conf.GlobalConfiguration{
JWT: ts.Config.JWT,
External: conf.ProviderConfiguration{
Phone: conf.PhoneProviderConfiguration{
Enabled: false,
},
},
},
userData: map[string]interface{}{
"phone": "123456789",
"password": "test1",
},
expected: http.StatusOK,
},
{
desc: "All Signups Disabled",
customConfig: &conf.GlobalConfiguration{
JWT: ts.Config.JWT,
DisableSignup: true,
},
userData: map[string]interface{}{
"email": "test2@example.com",
"password": "test2",
},
expected: http.StatusOK,
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
// Initialize user data
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.userData))
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/admin/users", &buffer)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.Config.JWT = c.customConfig.JWT
ts.Config.External = c.customConfig.External
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), c.expected, w.Code)
})
}
}
// TestAdminUserDeleteFactor tests API /admin/users/<user_id>/factors/<factor_id>/
func (ts *AdminTestSuite) TestAdminUserDeleteFactor() {
u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
f := models.NewTOTPFactor(u, "testSimpleName")
require.NoError(ts.T(), f.UpdateStatus(ts.API.db, models.FactorStateVerified))
require.NoError(ts.T(), f.SetSecret("secretkey", ts.Config.Security.DBEncryption.Encrypt, ts.Config.Security.DBEncryption.EncryptionKeyID, ts.Config.Security.DBEncryption.EncryptionKey))
require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor")
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s/factors/%s/", u.ID, f.ID), nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
_, err = models.FindFactorByFactorID(ts.API.db, f.ID)
require.EqualError(ts.T(), err, models.FactorNotFoundError{}.Error())
}
// TestAdminUserGetFactor tests API /admin/user/<user_id>/factors/
func (ts *AdminTestSuite) TestAdminUserGetFactors() {
u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
f := models.NewTOTPFactor(u, "testSimpleName")
require.NoError(ts.T(), f.SetSecret("secretkey", ts.Config.Security.DBEncryption.Encrypt, ts.Config.Security.DBEncryption.EncryptionKeyID, ts.Config.Security.DBEncryption.EncryptionKey))
require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor")
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/admin/users/%s/factors/", u.ID), nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
getFactorsResp := []*models.Factor{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&getFactorsResp))
require.Equal(ts.T(), getFactorsResp[0].Secret, "")
}
func (ts *AdminTestSuite) TestAdminUserUpdateFactor() {
u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
f := models.NewPhoneFactor(u, "123456789", "testSimpleName")
require.NoError(ts.T(), f.SetSecret("secretkey", ts.Config.Security.DBEncryption.Encrypt, ts.Config.Security.DBEncryption.EncryptionKeyID, ts.Config.Security.DBEncryption.EncryptionKey))
require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor")
var cases = []struct {
Desc string
FactorData map[string]interface{}
ExpectedCode int
}{
{
Desc: "Update Factor friendly name",
FactorData: map[string]interface{}{
"friendly_name": "john",
},
ExpectedCode: http.StatusOK,
},
{
Desc: "Update Factor phone number",
FactorData: map[string]interface{}{
"phone": "+1976154321",
},
ExpectedCode: http.StatusOK,
},
}
// Initialize factor data
for _, c := range cases {
ts.Run(c.Desc, func() {
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.FactorData))
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/admin/users/%s/factors/%s/", u.ID, f.ID), &buffer)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), c.ExpectedCode, w.Code)
})
}
}
func (ts *AdminTestSuite) TestAdminUserCreateValidationErrors() {
cases := []struct {
desc string
params map[string]interface{}
}{
{
desc: "create user without email and phone",
params: map[string]interface{}{
"password": "test_password",
},
},
{
desc: "create user with password and password hash",
params: map[string]interface{}{
"email": "test@example.com",
"password": "test_password",
"password_hash": "$2y$10$Tk6yEdmTbb/eQ/haDMaCsuCsmtPVprjHMcij1RqiJdLGPDXnL3L1a",
},
},
{
desc: "invalid ban duration",
params: map[string]interface{}{
"email": "test@example.com",
"ban_duration": "never",
},
},
{
desc: "custom id is nil",
params: map[string]interface{}{
"id": "00000000-0000-0000-0000-000000000000",
"email": "test@example.com",
},
},
{
desc: "bad id format",
params: map[string]interface{}{
"id": "bad_uuid_format",
"email": "test@example.com",
},
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.params))
req := httptest.NewRequest(http.MethodPost, "/admin/users", &buffer)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusBadRequest, w.Code, w)
data := map[string]interface{}{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
require.Equal(ts.T(), data["error_code"], ErrorCodeValidationFailed)
})
}
}

View File

@ -0,0 +1,55 @@
package api
import (
"net/http"
"github.com/supabase/auth/internal/metering"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/storage"
)
func (a *API) SignupAnonymously(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
config := a.config
db := a.db.WithContext(ctx)
aud := a.requestAud(ctx, r)
if config.DisableSignup {
return unprocessableEntityError(ErrorCodeSignupDisabled, "Signups not allowed for this instance")
}
params := &SignupParams{}
if err := retrieveRequestParams(r, params); err != nil {
return err
}
params.Aud = aud
params.Provider = "anonymous"
newUser, err := params.ToUserModel(false /* <- isSSOUser */)
if err != nil {
return err
}
var grantParams models.GrantParams
grantParams.FillGrantParams(r)
var token *AccessTokenResponse
err = db.Transaction(func(tx *storage.Connection) error {
var terr error
newUser, terr = a.signupNewUser(tx, newUser)
if terr != nil {
return terr
}
token, terr = a.issueRefreshToken(r, tx, newUser, models.Anonymous, grantParams)
if terr != nil {
return terr
}
return nil
})
if err != nil {
return internalServerError("Database error creating anonymous user").WithInternalError(err)
}
metering.RecordLogin("anonymous", newUser.ID)
return sendJSON(w, http.StatusOK, token)
}

View File

@ -0,0 +1,329 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofrs/uuid"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
mail "github.com/supabase/auth/internal/mailer"
"github.com/supabase/auth/internal/models"
)
type AnonymousTestSuite struct {
suite.Suite
API *API
Config *conf.GlobalConfiguration
}
func TestAnonymous(t *testing.T) {
api, config, err := setupAPIForTest()
require.NoError(t, err)
ts := &AnonymousTestSuite{
API: api,
Config: config,
}
defer api.db.Close()
suite.Run(t, ts)
}
func (ts *AnonymousTestSuite) SetupTest() {
models.TruncateAll(ts.API.db)
// Create anonymous user
params := &SignupParams{
Aud: ts.Config.JWT.Aud,
Provider: "anonymous",
}
u, err := params.ToUserModel(false)
require.NoError(ts.T(), err, "Error creating test user model")
require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new anonymous test user")
}
func (ts *AnonymousTestSuite) TestAnonymousLogins() {
ts.Config.External.AnonymousUsers.Enabled = true
// Request body
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"data": map[string]interface{}{
"field": "foo",
},
}))
req := httptest.NewRequest(http.MethodPost, "/signup", &buffer)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
data := &AccessTokenResponse{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
assert.NotEmpty(ts.T(), data.User.ID)
assert.Equal(ts.T(), ts.Config.JWT.Aud, data.User.Aud)
assert.Empty(ts.T(), data.User.GetEmail())
assert.Empty(ts.T(), data.User.GetPhone())
assert.True(ts.T(), data.User.IsAnonymous)
assert.Equal(ts.T(), models.JSONMap(models.JSONMap{"field": "foo"}), data.User.UserMetaData)
}
func (ts *AnonymousTestSuite) TestConvertAnonymousUserToPermanent() {
ts.Config.External.AnonymousUsers.Enabled = true
ts.Config.Sms.TestOTP = map[string]string{"1234567890": "000000", "1234560000": "000000"}
// test OTPs still require setting up an sms provider
ts.Config.Sms.Provider = "twilio"
ts.Config.Sms.Twilio.AccountSid = "fake-sid"
ts.Config.Sms.Twilio.AuthToken = "fake-token"
ts.Config.Sms.Twilio.MessageServiceSid = "fake-message-service-sid"
cases := []struct {
desc string
body map[string]interface{}
verificationType string
}{
{
desc: "convert anonymous user to permanent user with email",
body: map[string]interface{}{
"email": "test@example.com",
},
verificationType: "email_change",
},
{
desc: "convert anonymous user to permanent user with phone",
body: map[string]interface{}{
"phone": "1234567890",
},
verificationType: "phone_change",
},
{
desc: "convert anonymous user to permanent user with email & password",
body: map[string]interface{}{
"email": "test2@example.com",
"password": "test-password",
},
verificationType: "email_change",
},
{
desc: "convert anonymous user to permanent user with phone & password",
body: map[string]interface{}{
"phone": "1234560000",
"password": "test-password",
},
verificationType: "phone_change",
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
// Request body
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{}))
req := httptest.NewRequest(http.MethodPost, "/signup", &buffer)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
signupResponse := &AccessTokenResponse{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&signupResponse))
// Add email to anonymous user
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.body))
req = httptest.NewRequest(http.MethodPut, "/user", &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signupResponse.Token))
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
// Check if anonymous user is still anonymous
user, err := models.FindUserByID(ts.API.db, signupResponse.User.ID)
require.NoError(ts.T(), err)
require.NotEmpty(ts.T(), user)
require.True(ts.T(), user.IsAnonymous)
// Check if user has a password set
if c.body["password"] != nil {
require.True(ts.T(), user.HasPassword())
}
switch c.verificationType {
case mail.EmailChangeVerification:
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"token_hash": user.EmailChangeTokenNew,
"type": c.verificationType,
}))
case phoneChangeVerification:
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"phone": user.PhoneChange,
"token": "000000",
"type": c.verificationType,
}))
}
req = httptest.NewRequest(http.MethodPost, "/verify", &buffer)
req.Header.Set("Content-Type", "application/json")
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
data := &AccessTokenResponse{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
// User is a permanent user and not anonymous anymore
assert.Equal(ts.T(), signupResponse.User.ID, data.User.ID)
assert.Equal(ts.T(), ts.Config.JWT.Aud, data.User.Aud)
assert.False(ts.T(), data.User.IsAnonymous)
// User should have an identity
assert.Len(ts.T(), data.User.Identities, 1)
switch c.verificationType {
case mail.EmailChangeVerification:
assert.Equal(ts.T(), c.body["email"], data.User.GetEmail())
assert.Equal(ts.T(), models.JSONMap(models.JSONMap{"provider": "email", "providers": []interface{}{"email"}}), data.User.AppMetaData)
assert.NotEmpty(ts.T(), data.User.EmailConfirmedAt)
case phoneChangeVerification:
assert.Equal(ts.T(), c.body["phone"], data.User.GetPhone())
assert.Equal(ts.T(), models.JSONMap(models.JSONMap{"provider": "phone", "providers": []interface{}{"phone"}}), data.User.AppMetaData)
assert.NotEmpty(ts.T(), data.User.PhoneConfirmedAt)
}
})
}
}
func (ts *AnonymousTestSuite) TestRateLimitAnonymousSignups() {
var buffer bytes.Buffer
ts.Config.External.AnonymousUsers.Enabled = true
// It rate limits after 30 requests
for i := 0; i < int(ts.Config.RateLimitAnonymousUsers); i++ {
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{}))
req := httptest.NewRequest(http.MethodPost, "http://localhost/signup", &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("My-Custom-Header", "1.2.3.4")
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusOK, w.Code)
}
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{}))
req := httptest.NewRequest(http.MethodPost, "http://localhost/signup", &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("My-Custom-Header", "1.2.3.4")
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusTooManyRequests, w.Code)
// It ignores X-Forwarded-For by default
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{}))
req.Header.Set("X-Forwarded-For", "1.1.1.1")
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusTooManyRequests, w.Code)
// It doesn't rate limit a new value for the limited header
req.Header.Set("My-Custom-Header", "5.6.7.8")
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusBadRequest, w.Code)
}
func (ts *AnonymousTestSuite) TestAdminUpdateAnonymousUser() {
claims := &AccessTokenClaims{
Role: "supabase_admin",
}
adminJwt, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(ts.Config.JWT.Secret))
require.NoError(ts.T(), err)
u1, err := models.NewUser("", "", "", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err)
u1.IsAnonymous = true
require.NoError(ts.T(), ts.API.db.Create(u1))
u2, err := models.NewUser("", "", "", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err)
u2.IsAnonymous = true
require.NoError(ts.T(), ts.API.db.Create(u2))
cases := []struct {
desc string
userId uuid.UUID
body map[string]interface{}
expected map[string]interface{}
expectedIdentities int
}{
{
desc: "update anonymous user with email and email confirm true",
userId: u1.ID,
body: map[string]interface{}{
"email": "foo@example.com",
"email_confirm": true,
},
expected: map[string]interface{}{
"email": "foo@example.com",
"is_anonymous": false,
},
expectedIdentities: 1,
},
{
desc: "update anonymous user with email and email confirm false",
userId: u2.ID,
body: map[string]interface{}{
"email": "bar@example.com",
"email_confirm": false,
},
expected: map[string]interface{}{
"email": "bar@example.com",
"is_anonymous": true,
},
expectedIdentities: 1,
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
// Request body
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.body))
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/admin/users/%s", c.userId), &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", adminJwt))
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
var data models.User
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
require.NotNil(ts.T(), data)
require.Len(ts.T(), data.Identities, c.expectedIdentities)
actual := map[string]interface{}{
"email": data.GetEmail(),
"is_anonymous": data.IsAnonymous,
}
require.Equal(ts.T(), c.expected, actual)
})
}
}

View File

@ -0,0 +1,318 @@
package api
import (
"net/http"
"regexp"
"time"
"github.com/rs/cors"
"github.com/sebest/xff"
"github.com/sirupsen/logrus"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/mailer"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/observability"
"github.com/supabase/auth/internal/storage"
"github.com/supabase/auth/internal/utilities"
"github.com/supabase/hibp"
)
const (
audHeaderName = "X-JWT-AUD"
defaultVersion = "unknown version"
)
var bearerRegexp = regexp.MustCompile(`^(?:B|b)earer (\S+$)`)
// API is the main REST API
type API struct {
handler http.Handler
db *storage.Connection
config *conf.GlobalConfiguration
version string
hibpClient *hibp.PwnedClient
// overrideTime can be used to override the clock used by handlers. Should only be used in tests!
overrideTime func() time.Time
limiterOpts *LimiterOptions
}
func (a *API) Now() time.Time {
if a.overrideTime != nil {
return a.overrideTime()
}
return time.Now()
}
// NewAPI instantiates a new REST API
func NewAPI(globalConfig *conf.GlobalConfiguration, db *storage.Connection, opt ...Option) *API {
return NewAPIWithVersion(globalConfig, db, defaultVersion, opt...)
}
func (a *API) deprecationNotices() {
config := a.config
log := logrus.WithField("component", "api")
if config.JWT.AdminGroupName != "" {
log.Warn("DEPRECATION NOTICE: GOTRUE_JWT_ADMIN_GROUP_NAME not supported by Supabase's GoTrue, will be removed soon")
}
if config.JWT.DefaultGroupName != "" {
log.Warn("DEPRECATION NOTICE: GOTRUE_JWT_DEFAULT_GROUP_NAME not supported by Supabase's GoTrue, will be removed soon")
}
}
// NewAPIWithVersion creates a new REST API using the specified version
func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Connection, version string, opt ...Option) *API {
api := &API{config: globalConfig, db: db, version: version}
for _, o := range opt {
o.apply(api)
}
if api.limiterOpts == nil {
api.limiterOpts = NewLimiterOptions(globalConfig)
}
if api.config.Password.HIBP.Enabled {
httpClient := &http.Client{
// all HIBP API requests should finish quickly to avoid
// unnecessary slowdowns
Timeout: 5 * time.Second,
}
api.hibpClient = &hibp.PwnedClient{
UserAgent: api.config.Password.HIBP.UserAgent,
HTTP: httpClient,
}
if api.config.Password.HIBP.Bloom.Enabled {
cache := utilities.NewHIBPBloomCache(api.config.Password.HIBP.Bloom.Items, api.config.Password.HIBP.Bloom.FalsePositives)
api.hibpClient.Cache = cache
logrus.Infof("Pwned passwords cache is %.2f KB", float64(cache.Cap())/(8*1024.0))
}
}
api.deprecationNotices()
xffmw, _ := xff.Default()
logger := observability.NewStructuredLogger(logrus.StandardLogger(), globalConfig)
r := newRouter()
r.UseBypass(observability.AddRequestID(globalConfig))
r.UseBypass(logger)
r.UseBypass(xffmw.Handler)
r.UseBypass(recoverer)
if globalConfig.API.MaxRequestDuration > 0 {
r.UseBypass(timeoutMiddleware(globalConfig.API.MaxRequestDuration))
}
// request tracing should be added only when tracing or metrics is enabled
if globalConfig.Tracing.Enabled || globalConfig.Metrics.Enabled {
r.UseBypass(observability.RequestTracing())
}
if globalConfig.DB.CleanupEnabled {
cleanup := models.NewCleanup(globalConfig)
r.UseBypass(api.databaseCleanup(cleanup))
}
r.Get("/health", api.HealthCheck)
r.Get("/.well-known/jwks.json", api.Jwks)
r.Route("/callback", func(r *router) {
r.Use(api.isValidExternalHost)
r.Use(api.loadFlowState)
r.Get("/", api.ExternalProviderCallback)
r.Post("/", api.ExternalProviderCallback)
})
r.Route("/", func(r *router) {
r.Use(api.isValidExternalHost)
r.Get("/settings", api.Settings)
r.Get("/authorize", api.ExternalProviderRedirect)
r.With(api.requireAdminCredentials).Post("/invite", api.Invite)
r.With(api.verifyCaptcha).Route("/signup", func(r *router) {
// rate limit per hour
limitAnonymousSignIns := api.limiterOpts.AnonymousSignIns
limitSignups := api.limiterOpts.Signups
r.Post("/", func(w http.ResponseWriter, r *http.Request) error {
params := &SignupParams{}
if err := retrieveRequestParams(r, params); err != nil {
return err
}
if params.Email == "" && params.Phone == "" {
if !api.config.External.AnonymousUsers.Enabled {
return unprocessableEntityError(ErrorCodeAnonymousProviderDisabled, "Anonymous sign-ins are disabled")
}
if _, err := api.limitHandler(limitAnonymousSignIns)(w, r); err != nil {
return err
}
return api.SignupAnonymously(w, r)
}
// apply ip-based rate limiting on otps
if _, err := api.limitHandler(limitSignups)(w, r); err != nil {
return err
}
return api.Signup(w, r)
})
})
r.With(api.limitHandler(api.limiterOpts.Recover)).
With(api.verifyCaptcha).With(api.requireEmailProvider).Post("/recover", api.Recover)
r.With(api.limitHandler(api.limiterOpts.Resend)).
With(api.verifyCaptcha).Post("/resend", api.Resend)
r.With(api.limitHandler(api.limiterOpts.MagicLink)).
With(api.verifyCaptcha).Post("/magiclink", api.MagicLink)
r.With(api.limitHandler(api.limiterOpts.Otp)).
With(api.verifyCaptcha).Post("/otp", api.Otp)
r.With(api.limitHandler(api.limiterOpts.Token)).
With(api.verifyCaptcha).Post("/token", api.Token)
r.With(api.limitHandler(api.limiterOpts.Verify)).Route("/verify", func(r *router) {
r.Get("/", api.Verify)
r.Post("/", api.Verify)
})
r.With(api.requireAuthentication).Post("/logout", api.Logout)
r.With(api.requireAuthentication).Route("/reauthenticate", func(r *router) {
r.Get("/", api.Reauthenticate)
})
r.With(api.requireAuthentication).Route("/user", func(r *router) {
r.Get("/", api.UserGet)
r.With(api.limitHandler(api.limiterOpts.User)).Put("/", api.UserUpdate)
r.Route("/identities", func(r *router) {
r.Use(api.requireManualLinkingEnabled)
r.Get("/authorize", api.LinkIdentity)
r.Delete("/{identity_id}", api.DeleteIdentity)
})
})
r.With(api.requireAuthentication).Route("/factors", func(r *router) {
r.Use(api.requireNotAnonymous)
r.Post("/", api.EnrollFactor)
r.Route("/{factor_id}", func(r *router) {
r.Use(api.loadFactor)
r.With(api.limitHandler(api.limiterOpts.FactorVerify)).
Post("/verify", api.VerifyFactor)
r.With(api.limitHandler(api.limiterOpts.FactorChallenge)).
Post("/challenge", api.ChallengeFactor)
r.Delete("/", api.UnenrollFactor)
})
})
r.Route("/sso", func(r *router) {
r.Use(api.requireSAMLEnabled)
r.With(api.limitHandler(api.limiterOpts.SSO)).
With(api.verifyCaptcha).Post("/", api.SingleSignOn)
r.Route("/saml", func(r *router) {
r.Get("/metadata", api.SAMLMetadata)
r.With(api.limitHandler(api.limiterOpts.SAMLAssertion)).
Post("/acs", api.SamlAcs)
})
})
r.Route("/admin", func(r *router) {
r.Use(api.requireAdminCredentials)
r.Route("/audit", func(r *router) {
r.Get("/", api.adminAuditLog)
})
r.Route("/users", func(r *router) {
r.Get("/", api.adminUsers)
r.Post("/", api.adminUserCreate)
r.Route("/{user_id}", func(r *router) {
r.Use(api.loadUser)
r.Route("/factors", func(r *router) {
r.Get("/", api.adminUserGetFactors)
r.Route("/{factor_id}", func(r *router) {
r.Use(api.loadFactor)
r.Delete("/", api.adminUserDeleteFactor)
r.Put("/", api.adminUserUpdateFactor)
})
})
r.Get("/", api.adminUserGet)
r.Put("/", api.adminUserUpdate)
r.Delete("/", api.adminUserDelete)
})
})
r.Post("/generate_link", api.adminGenerateLink)
r.Route("/sso", func(r *router) {
r.Route("/providers", func(r *router) {
r.Get("/", api.adminSSOProvidersList)
r.Post("/", api.adminSSOProvidersCreate)
r.Route("/{idp_id}", func(r *router) {
r.Use(api.loadSSOProvider)
r.Get("/", api.adminSSOProvidersGet)
r.Put("/", api.adminSSOProvidersUpdate)
r.Delete("/", api.adminSSOProvidersDelete)
})
})
})
})
})
corsHandler := cors.New(cors.Options{
AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
AllowedHeaders: globalConfig.CORS.AllAllowedHeaders([]string{"Accept", "Authorization", "Content-Type", "X-Client-IP", "X-Client-Info", audHeaderName, useCookieHeader, APIVersionHeaderName}),
ExposedHeaders: []string{"X-Total-Count", "Link", APIVersionHeaderName},
AllowCredentials: true,
})
api.handler = corsHandler.Handler(r)
return api
}
type HealthCheckResponse struct {
Version string `json:"version"`
Name string `json:"name"`
Description string `json:"description"`
}
// HealthCheck endpoint indicates if the gotrue api service is available
func (a *API) HealthCheck(w http.ResponseWriter, r *http.Request) error {
return sendJSON(w, http.StatusOK, HealthCheckResponse{
Version: a.version,
Name: "GoTrue",
Description: "GoTrue is a user registration and authentication API",
})
}
// Mailer returns NewMailer with the current tenant config
func (a *API) Mailer() mailer.Mailer {
config := a.config
return mailer.NewMailer(config)
}
// ServeHTTP implements the http.Handler interface by passing the request along
// to its underlying Handler.
func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.handler.ServeHTTP(w, r)
}

View File

@ -0,0 +1,57 @@
package api
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/crypto"
"github.com/supabase/auth/internal/storage"
"github.com/supabase/auth/internal/storage/test"
)
const (
apiTestVersion = "1"
apiTestConfig = "../../hack/test.env"
)
func init() {
crypto.PasswordHashCost = crypto.QuickHashCost
}
// setupAPIForTest creates a new API to run tests with.
// Using this function allows us to keep track of the database connection
// and cleaning up data between tests.
func setupAPIForTest() (*API, *conf.GlobalConfiguration, error) {
return setupAPIForTestWithCallback(nil)
}
func setupAPIForTestWithCallback(cb func(*conf.GlobalConfiguration, *storage.Connection)) (*API, *conf.GlobalConfiguration, error) {
config, err := conf.LoadGlobal(apiTestConfig)
if err != nil {
return nil, nil, err
}
if cb != nil {
cb(config, nil)
}
conn, err := test.SetupDBConnection(config)
if err != nil {
return nil, nil, err
}
if cb != nil {
cb(nil, conn)
}
limiterOpts := NewLimiterOptions(config)
return NewAPIWithVersion(config, conn, apiTestVersion, limiterOpts), config, nil
}
func TestEmailEnabledByDefault(t *testing.T) {
api, _, err := setupAPIForTest()
require.NoError(t, err)
require.True(t, api.config.External.Email.Enabled)
}

View File

@ -0,0 +1,35 @@
package api
import (
"time"
)
const APIVersionHeaderName = "X-Supabase-Api-Version"
type APIVersion = time.Time
var (
APIVersionInitial = time.Time{}
APIVersion20240101 = time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)
)
func DetermineClosestAPIVersion(date string) (APIVersion, error) {
if date == "" {
return APIVersionInitial, nil
}
parsed, err := time.ParseInLocation("2006-01-02", date, time.UTC)
if err != nil {
return APIVersionInitial, err
}
if parsed.Compare(APIVersion20240101) >= 0 {
return APIVersion20240101, nil
}
return APIVersionInitial, nil
}
func FormatAPIVersion(apiVersion APIVersion) string {
return apiVersion.Format("2006-01-02")
}

View File

@ -0,0 +1,29 @@
package api
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestDetermineClosestAPIVersion(t *testing.T) {
version, err := DetermineClosestAPIVersion("")
require.NoError(t, err)
require.Equal(t, APIVersionInitial, version)
version, err = DetermineClosestAPIVersion("Not a date")
require.Error(t, err)
require.Equal(t, APIVersionInitial, version)
version, err = DetermineClosestAPIVersion("2023-12-31")
require.NoError(t, err)
require.Equal(t, APIVersionInitial, version)
version, err = DetermineClosestAPIVersion("2024-01-01")
require.NoError(t, err)
require.Equal(t, APIVersion20240101, version)
version, err = DetermineClosestAPIVersion("2024-01-02")
require.NoError(t, err)
require.Equal(t, APIVersion20240101, version)
}

View File

@ -0,0 +1,47 @@
package api
import (
"net/http"
"strings"
"github.com/supabase/auth/internal/models"
)
var filterColumnMap = map[string][]string{
"author": {"actor_username", "actor_name"},
"action": {"action"},
"type": {"log_type"},
}
func (a *API) adminAuditLog(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)
// aud := a.requestAud(ctx, r)
pageParams, err := paginate(r)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "Bad Pagination Parameters: %v", err)
}
var col []string
var qval string
q := r.URL.Query().Get("query")
if q != "" {
var exists bool
qparts := strings.SplitN(q, ":", 2)
col, exists = filterColumnMap[qparts[0]]
if !exists || len(qparts) < 2 {
return badRequestError(ErrorCodeValidationFailed, "Invalid query scope: %s", q)
}
qval = qparts[1]
}
logs, err := models.FindAuditLogEntries(db, col, qval, pageParams)
if err != nil {
return internalServerError("Error searching for audit logs").WithInternalError(err)
}
addPaginationHeaders(w, r, pageParams)
return sendJSON(w, http.StatusOK, logs)
}

View File

@ -0,0 +1,139 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/models"
)
type AuditTestSuite struct {
suite.Suite
API *API
Config *conf.GlobalConfiguration
token string
}
func TestAudit(t *testing.T) {
api, config, err := setupAPIForTest()
require.NoError(t, err)
ts := &AuditTestSuite{
API: api,
Config: config,
}
defer api.db.Close()
suite.Run(t, ts)
}
func (ts *AuditTestSuite) SetupTest() {
models.TruncateAll(ts.API.db)
ts.token = ts.makeSuperAdmin("")
}
func (ts *AuditTestSuite) makeSuperAdmin(email string) string {
u, err := models.NewUser("", email, "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test User"})
require.NoError(ts.T(), err, "Error making new user")
u.Role = "supabase_admin"
require.NoError(ts.T(), ts.API.db.Create(u))
session, err := models.NewSession(u.ID, nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(session))
var token string
req := httptest.NewRequest(http.MethodPost, "/token?grant_type=password", nil)
token, _, err = ts.API.generateAccessToken(req, ts.API.db, u, &session.ID, models.PasswordGrant)
require.NoError(ts.T(), err, "Error generating access token")
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
require.NoError(ts.T(), err, "Error parsing token")
return token
}
func (ts *AuditTestSuite) TestAuditGet() {
ts.prepareDeleteEvent()
// CHECK FOR AUDIT LOG
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/admin/audit", nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
assert.Equal(ts.T(), "</admin/audit?page=1>; rel=\"last\"", w.Header().Get("Link"))
assert.Equal(ts.T(), "1", w.Header().Get("X-Total-Count"))
logs := []models.AuditLogEntry{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&logs))
require.Len(ts.T(), logs, 1)
require.Contains(ts.T(), logs[0].Payload, "actor_username")
assert.Equal(ts.T(), "supabase_admin", logs[0].Payload["actor_username"])
traits, ok := logs[0].Payload["traits"].(map[string]interface{})
require.True(ts.T(), ok)
require.Contains(ts.T(), traits, "user_email")
assert.Equal(ts.T(), "test-delete@example.com", traits["user_email"])
}
func (ts *AuditTestSuite) TestAuditFilters() {
ts.prepareDeleteEvent()
queries := []string{
"/admin/audit?query=action:user_deleted",
"/admin/audit?query=type:team",
"/admin/audit?query=author:admin",
}
for _, q := range queries {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, q, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
logs := []models.AuditLogEntry{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&logs))
require.Len(ts.T(), logs, 1)
require.Contains(ts.T(), logs[0].Payload, "actor_username")
assert.Equal(ts.T(), "supabase_admin", logs[0].Payload["actor_username"])
traits, ok := logs[0].Payload["traits"].(map[string]interface{})
require.True(ts.T(), ok)
require.Contains(ts.T(), traits, "user_email")
assert.Equal(ts.T(), "test-delete@example.com", traits["user_email"])
}
}
func (ts *AuditTestSuite) prepareDeleteEvent() {
// DELETE USER
u, err := models.NewUser("12345678", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
// Setup request
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s", u.ID), nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
}

View File

@ -0,0 +1,141 @@
package api
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/gofrs/uuid"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/storage"
)
// requireAuthentication checks incoming requests for tokens presented using the Authorization header
func (a *API) requireAuthentication(w http.ResponseWriter, r *http.Request) (context.Context, error) {
token, err := a.extractBearerToken(r)
if err != nil {
return nil, err
}
ctx, err := a.parseJWTClaims(token, r)
if err != nil {
return ctx, err
}
ctx, err = a.maybeLoadUserOrSession(ctx)
if err != nil {
return ctx, err
}
return ctx, err
}
func (a *API) requireNotAnonymous(w http.ResponseWriter, r *http.Request) (context.Context, error) {
ctx := r.Context()
claims := getClaims(ctx)
if claims.IsAnonymous {
return nil, forbiddenError(ErrorCodeNoAuthorization, "Anonymous user not allowed to perform these actions")
}
return ctx, nil
}
func (a *API) requireAdmin(ctx context.Context) (context.Context, error) {
// Find the administrative user
claims := getClaims(ctx)
if claims == nil {
return nil, forbiddenError(ErrorCodeBadJWT, "Invalid token")
}
adminRoles := a.config.JWT.AdminRoles
if isStringInSlice(claims.Role, adminRoles) {
// successful authentication
return withAdminUser(ctx, &models.User{Role: claims.Role, Email: storage.NullString(claims.Role)}), nil
}
return nil, forbiddenError(ErrorCodeNotAdmin, "User not allowed").WithInternalMessage(fmt.Sprintf("this token needs to have one of the following roles: %v", strings.Join(adminRoles, ", ")))
}
func (a *API) extractBearerToken(r *http.Request) (string, error) {
authHeader := r.Header.Get("Authorization")
matches := bearerRegexp.FindStringSubmatch(authHeader)
if len(matches) != 2 {
return "", httpError(http.StatusUnauthorized, ErrorCodeNoAuthorization, "This endpoint requires a Bearer token")
}
return matches[1], nil
}
func (a *API) parseJWTClaims(bearer string, r *http.Request) (context.Context, error) {
ctx := r.Context()
config := a.config
p := jwt.NewParser(jwt.WithValidMethods(config.JWT.ValidMethods))
token, err := p.ParseWithClaims(bearer, &AccessTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
if kid, ok := token.Header["kid"]; ok {
if kidStr, ok := kid.(string); ok {
return conf.FindPublicKeyByKid(kidStr, &config.JWT)
}
}
if alg, ok := token.Header["alg"]; ok {
if alg == jwt.SigningMethodHS256.Name {
// preserve backward compatibility for cases where the kid is not set
return []byte(config.JWT.Secret), nil
}
}
return nil, fmt.Errorf("missing kid")
})
if err != nil {
return nil, forbiddenError(ErrorCodeBadJWT, "invalid JWT: unable to parse or verify signature, %v", err).WithInternalError(err)
}
return withToken(ctx, token), nil
}
func (a *API) maybeLoadUserOrSession(ctx context.Context) (context.Context, error) {
db := a.db.WithContext(ctx)
claims := getClaims(ctx)
if claims == nil {
return ctx, forbiddenError(ErrorCodeBadJWT, "invalid token: missing claims")
}
if claims.Subject == "" {
return nil, forbiddenError(ErrorCodeBadJWT, "invalid claim: missing sub claim")
}
var user *models.User
if claims.Subject != "" {
userId, err := uuid.FromString(claims.Subject)
if err != nil {
return ctx, badRequestError(ErrorCodeBadJWT, "invalid claim: sub claim must be a UUID").WithInternalError(err)
}
user, err = models.FindUserByID(db, userId)
if err != nil {
if models.IsNotFoundError(err) {
return ctx, forbiddenError(ErrorCodeUserNotFound, "User from sub claim in JWT does not exist")
}
return ctx, err
}
ctx = withUser(ctx, user)
}
var session *models.Session
if claims.SessionId != "" && claims.SessionId != uuid.Nil.String() {
sessionId, err := uuid.FromString(claims.SessionId)
if err != nil {
return ctx, forbiddenError(ErrorCodeBadJWT, "invalid claim: session_id claim must be a UUID").WithInternalError(err)
}
session, err = models.FindSessionByID(db, sessionId, false)
if err != nil {
if models.IsNotFoundError(err) {
return ctx, forbiddenError(ErrorCodeSessionNotFound, "Session from session_id claim in JWT does not exist").WithInternalError(err).WithInternalMessage(fmt.Sprintf("session id (%s) doesn't exist", sessionId))
}
return ctx, err
}
ctx = withSession(ctx, session)
}
return ctx, nil
}

View File

@ -0,0 +1,284 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofrs/uuid"
jwt "github.com/golang-jwt/jwt/v5"
jwk "github.com/lestrrat-go/jwx/v2/jwk"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/models"
)
type AuthTestSuite struct {
suite.Suite
API *API
Config *conf.GlobalConfiguration
}
func TestAuth(t *testing.T) {
api, config, err := setupAPIForTest()
require.NoError(t, err)
ts := &AuthTestSuite{
API: api,
Config: config,
}
defer api.db.Close()
suite.Run(t, ts)
}
func (ts *AuthTestSuite) SetupTest() {
models.TruncateAll(ts.API.db)
// Create user
u, err := models.NewUser("", "test@example.com", "password", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error creating test user model")
require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user")
}
func (ts *AuthTestSuite) TestExtractBearerToken() {
userClaims := &AccessTokenClaims{
Role: "authenticated",
}
userJwt, err := jwt.NewWithClaims(jwt.SigningMethodHS256, userClaims).SignedString([]byte(ts.Config.JWT.Secret))
require.NoError(ts.T(), err)
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Set("Authorization", "Bearer "+userJwt)
token, err := ts.API.extractBearerToken(req)
require.NoError(ts.T(), err)
require.Equal(ts.T(), userJwt, token)
}
func (ts *AuthTestSuite) TestParseJWTClaims() {
cases := []struct {
desc string
key map[string]interface{}
}{
{
desc: "HMAC key",
key: map[string]interface{}{
"kty": "oct",
"k": "S1LgKUjeqXDEolv9WPtjUpADVMHU_KYu8uRDrM-pDGg",
"kid": "ac50c3cc-9cf7-4fd6-a11f-fe066fd39118",
"key_ops": []string{"sign", "verify"},
"alg": "HS256",
},
},
{
desc: "RSA key",
key: map[string]interface{}{
"kty": "RSA",
"n": "2g0B_hMIx5ZPuTUtLRpRr0k314XniYm3AUFgR5FmTZIjrn7vLwsWij-2egGZeHa-y9ypAgB9Q-lQ3AlT7RMPiCIyLQI6TTC8k10NEnj8c0QZwENx1Qr8aBbuZbOP9Cz30EMWZSbzMbz7r8-3rp5wBRBtIPnLlbfZh_p0iBaJfB77-r_mvhOIFM4xS7ef3nkE96dnvbEN5a-HfjzDJIAt-LniUvzMWW2gQcmHiM4oeijE3PHesapLMt2JpsMhSRo8L7tysags9VMoyZ1GnpCdjtRwb_KpY9QTjV6lL8G5nsKFH7bhABYcpjDOvqkfT5nPXj6C7oCo6MPRirPWUTbq2w",
"e": "AQAB",
"d": "OOTj_DNjOxCRRLYHT5lqbt4f3_BkdZKlWYKBaKsbkmnrPYCJUDEIdJIjPrpkHPZ-2hp9TrRp-upJ2t_kMhujFdY2WWAXbkSlL5475vICjODcBzqR3RC8wzwYgBjWGtQQ5RpcIZCELBovYbRFLR7SA8BBeTU0VaBe9gf3l_qpbOT9QIl268uFdWndTjpehGLQRmAtR1snhvTha0b9nsBZsM_K-EfnoF7Q_lPsjwWDvIGpFXao8Ifaa_sFtQkHjHVBMW2Qgx3ZSrEva_brk7w0MNSYI7Nsmr56xFOpFRwZy0v8ZtgQZ4hXmUInRHIoQ2APeds9YmemojvJKVflt9pLIQ",
"p": "-o2hdQ5Z35cIS5APTVULj_BMoPJpgkuX-PSYC1SeBeff9K04kG5zrFMWJy_-27-ys4q754lpNwJdX2CjN1nb6qyn-uKP8B2oLayKs9ebkiOqvm3S2Xblvi_F8x6sOLba3lTYHK8G7U9aMB9U0mhAzzMFdw15XXusVFDvk-zxL28",
"q": "3sp-7HzZE_elKRmebjivcDhkXO2GrcN3EIqYbbXssHZFXJwVE9oc2CErGWa7QetOCr9C--ZuTmX0X3L--CoYr-hMB0dN8lcAhapr3aau-4i7vE3DWSUdcFSyi0BBDg8pWQWbxNyTXBuWeh1cnRBsLjCxAOVTF0y3_BnVR7mbBVU",
"dp": "DuYHGMfOrk3zz1J0pnuNIXT_iX6AqZ_HHKWmuN3CO8Wq-oimWWhH9pJGOfRPqk9-19BDFiSEniHE3ZwIeI0eV5kGsBNyzatlybl90e3bMVhvmb08EXRRevqqQaesQ_8Tiq7u3t3Fgqz6RuxGBfDvEaMOCyNA-T8WYzkg1eH8AX8",
"dq": "opOCK3CvuDJvA57-TdBvtaRxGJ78OLD6oceBlA29useTthDwEJyJj-4kVVTyMRhUyuLnLoro06zytvRjuxR9D2CkmmseJkn2x5OlQwnvhv4wgSj99H9xDBfCcntg_bFyqtO859tObVh0ZogmnTbuuoYtpEm0aLxDRmRTjxOSXEE",
"qi": "8skVE7BDASHXytKSWYbkxD0B3WpXic2rtnLgiMgasdSxul8XwcB-vjVSZprVrxkcmm6ZhszoxOlq8yylBmMvAnG_gEzTls_xapeuEXGYiGaTcpkCt1r-tBKcQkka2SayaWwAljsX4xSw-zKP2koUkEET_tIcbBOW1R4OWfRGqOI",
"kid": "0d24b26c-b3ec-4c02-acfd-d5a54d50b3a4",
"key_ops": []string{"sign", "verify"},
"alg": "RS256",
},
},
{
desc: "EC key",
key: map[string]interface{}{
"kty": "EC",
"x": "5wsOh-DrNPpm9KkuydtgGs_cv3oNvtR9OdXywt12aS4",
"y": "0y01ZbuH_VQjMEd8fcYaLdiv25EVJ5GOrb79dJJsqrM",
"crv": "P-256",
"d": "EDP4ReMMpAUcf82EF3JYvkm8C5hVAh258Rj6f3HTx7c",
"kid": "10646a77-f470-44a8-8400-2f988d9c9c1a",
"key_ops": []string{"sign", "verify"},
"alg": "ES256",
},
},
{
desc: "Ed25519 key",
key: map[string]interface{}{
"crv": "Ed25519",
"d": "jVpCLvOxatVkKe1MW9nFRn6Q8VVZPq5yziKU_Z0Yu-c",
"x": "YDkGdufJBQEPO6ylvd9IKfZlzvm9tOG5VCDpkJSSkiA",
"kty": "OKP",
"kid": "ec5e7a96-ea66-456c-826c-d8d6cb928c0f",
"key_ops": []string{"sign", "verify"},
"alg": "EdDSA",
},
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
bytes, err := json.Marshal(c.key)
require.NoError(ts.T(), err)
privKey, err := jwk.ParseKey(bytes)
require.NoError(ts.T(), err)
pubKey, err := privKey.PublicKey()
require.NoError(ts.T(), err)
ts.Config.JWT.Keys = conf.JwtKeysDecoder{privKey.KeyID(): conf.JwkInfo{
PublicKey: pubKey,
PrivateKey: privKey,
}}
ts.Config.JWT.ValidMethods = nil
require.NoError(ts.T(), ts.Config.ApplyDefaults())
userClaims := &AccessTokenClaims{
Role: "authenticated",
}
// get signing key and method from config
jwk, err := conf.GetSigningJwk(&ts.Config.JWT)
require.NoError(ts.T(), err)
signingMethod := conf.GetSigningAlg(jwk)
signingKey, err := conf.GetSigningKey(jwk)
require.NoError(ts.T(), err)
userJwtToken := jwt.NewWithClaims(signingMethod, userClaims)
require.NoError(ts.T(), err)
userJwtToken.Header["kid"] = jwk.KeyID()
userJwt, err := userJwtToken.SignedString(signingKey)
require.NoError(ts.T(), err)
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Set("Authorization", "Bearer "+userJwt)
ctx, err := ts.API.parseJWTClaims(userJwt, req)
require.NoError(ts.T(), err)
// check if token is stored in context
token := getToken(ctx)
require.Equal(ts.T(), userJwt, token.Raw)
})
}
}
func (ts *AuthTestSuite) TestMaybeLoadUserOrSession() {
u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
s, err := models.NewSession(u.ID, nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(s))
require.NoError(ts.T(), ts.API.db.Load(s))
cases := []struct {
Desc string
UserJwtClaims *AccessTokenClaims
ExpectedError error
ExpectedUser *models.User
ExpectedSession *models.Session
}{
{
Desc: "Missing Subject Claim",
UserJwtClaims: &AccessTokenClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: "",
},
Role: "authenticated",
},
ExpectedError: forbiddenError(ErrorCodeBadJWT, "invalid claim: missing sub claim"),
ExpectedUser: nil,
},
{
Desc: "Valid Subject Claim",
UserJwtClaims: &AccessTokenClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: u.ID.String(),
},
Role: "authenticated",
},
ExpectedError: nil,
ExpectedUser: u,
},
{
Desc: "Invalid Subject Claim",
UserJwtClaims: &AccessTokenClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: "invalid-subject-claim",
},
Role: "authenticated",
},
ExpectedError: badRequestError(ErrorCodeBadJWT, "invalid claim: sub claim must be a UUID"),
ExpectedUser: nil,
},
{
Desc: "Empty Session ID Claim",
UserJwtClaims: &AccessTokenClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: u.ID.String(),
},
Role: "authenticated",
SessionId: "",
},
ExpectedError: nil,
ExpectedUser: u,
},
{
Desc: "Invalid Session ID Claim",
UserJwtClaims: &AccessTokenClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: u.ID.String(),
},
Role: "authenticated",
SessionId: uuid.Nil.String(),
},
ExpectedError: nil,
ExpectedUser: u,
},
{
Desc: "Valid Session ID Claim",
UserJwtClaims: &AccessTokenClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: u.ID.String(),
},
Role: "authenticated",
SessionId: s.ID.String(),
},
ExpectedError: nil,
ExpectedUser: u,
ExpectedSession: s,
},
{
Desc: "Session ID doesn't exist",
UserJwtClaims: &AccessTokenClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: u.ID.String(),
},
Role: "authenticated",
SessionId: "73bf9ee0-9e8c-453b-b484-09cb93e2f341",
},
ExpectedError: forbiddenError(ErrorCodeSessionNotFound, "Session from session_id claim in JWT does not exist").WithInternalError(models.SessionNotFoundError{}).WithInternalMessage("session id (73bf9ee0-9e8c-453b-b484-09cb93e2f341) doesn't exist"),
ExpectedUser: u,
ExpectedSession: nil,
},
}
for _, c := range cases {
ts.Run(c.Desc, func() {
userJwt, err := jwt.NewWithClaims(jwt.SigningMethodHS256, c.UserJwtClaims).SignedString([]byte(ts.Config.JWT.Secret))
require.NoError(ts.T(), err)
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Set("Authorization", "Bearer "+userJwt)
ctx, err := ts.API.parseJWTClaims(userJwt, req)
require.NoError(ts.T(), err)
ctx, err = ts.API.maybeLoadUserOrSession(ctx)
if c.ExpectedError != nil {
require.Equal(ts.T(), c.ExpectedError.Error(), err.Error())
} else {
require.Equal(ts.T(), c.ExpectedError, err)
}
require.Equal(ts.T(), c.ExpectedUser, getUser(ctx))
require.Equal(ts.T(), c.ExpectedSession, getSession(ctx))
})
}
}

View File

@ -0,0 +1,243 @@
package api
import (
"context"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/supabase/auth/internal/models"
)
type contextKey string
func (c contextKey) String() string {
return "gotrue api context key " + string(c)
}
const (
tokenKey = contextKey("jwt")
inviteTokenKey = contextKey("invite_token")
signatureKey = contextKey("signature")
externalProviderTypeKey = contextKey("external_provider_type")
userKey = contextKey("user")
targetUserKey = contextKey("target_user")
factorKey = contextKey("factor")
sessionKey = contextKey("session")
externalReferrerKey = contextKey("external_referrer")
functionHooksKey = contextKey("function_hooks")
adminUserKey = contextKey("admin_user")
oauthTokenKey = contextKey("oauth_token") // for OAuth1.0, also known as request token
oauthVerifierKey = contextKey("oauth_verifier")
ssoProviderKey = contextKey("sso_provider")
externalHostKey = contextKey("external_host")
flowStateKey = contextKey("flow_state_id")
)
// withToken adds the JWT token to the context.
func withToken(ctx context.Context, token *jwt.Token) context.Context {
return context.WithValue(ctx, tokenKey, token)
}
// getToken reads the JWT token from the context.
func getToken(ctx context.Context) *jwt.Token {
obj := ctx.Value(tokenKey)
if obj == nil {
return nil
}
return obj.(*jwt.Token)
}
func getClaims(ctx context.Context) *AccessTokenClaims {
token := getToken(ctx)
if token == nil {
return nil
}
return token.Claims.(*AccessTokenClaims)
}
// withUser adds the user to the context.
func withUser(ctx context.Context, u *models.User) context.Context {
return context.WithValue(ctx, userKey, u)
}
// withTargetUser adds the target user for linking to the context.
func withTargetUser(ctx context.Context, u *models.User) context.Context {
return context.WithValue(ctx, targetUserKey, u)
}
// with Factor adds the factor id to the context.
func withFactor(ctx context.Context, f *models.Factor) context.Context {
return context.WithValue(ctx, factorKey, f)
}
// getUser reads the user from the context.
func getUser(ctx context.Context) *models.User {
if ctx == nil {
return nil
}
obj := ctx.Value(userKey)
if obj == nil {
return nil
}
return obj.(*models.User)
}
// getTargetUser reads the user from the context.
func getTargetUser(ctx context.Context) *models.User {
if ctx == nil {
return nil
}
obj := ctx.Value(targetUserKey)
if obj == nil {
return nil
}
return obj.(*models.User)
}
// getFactor reads the factor id from the context
func getFactor(ctx context.Context) *models.Factor {
obj := ctx.Value(factorKey)
if obj == nil {
return nil
}
return obj.(*models.Factor)
}
// withSession adds the session to the context.
func withSession(ctx context.Context, s *models.Session) context.Context {
return context.WithValue(ctx, sessionKey, s)
}
// getSession reads the session from the context.
func getSession(ctx context.Context) *models.Session {
if ctx == nil {
return nil
}
obj := ctx.Value(sessionKey)
if obj == nil {
return nil
}
return obj.(*models.Session)
}
// withSignature adds the provided request ID to the context.
func withSignature(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, signatureKey, id)
}
func withInviteToken(ctx context.Context, token string) context.Context {
return context.WithValue(ctx, inviteTokenKey, token)
}
func withFlowStateID(ctx context.Context, FlowStateID string) context.Context {
return context.WithValue(ctx, flowStateKey, FlowStateID)
}
func getFlowStateID(ctx context.Context) string {
obj := ctx.Value(flowStateKey)
if obj == nil {
return ""
}
return obj.(string)
}
func getInviteToken(ctx context.Context) string {
obj := ctx.Value(inviteTokenKey)
if obj == nil {
return ""
}
return obj.(string)
}
// withExternalProviderType adds the provided request ID to the context.
func withExternalProviderType(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, externalProviderTypeKey, id)
}
// getExternalProviderType reads the request ID from the context.
func getExternalProviderType(ctx context.Context) string {
obj := ctx.Value(externalProviderTypeKey)
if obj == nil {
return ""
}
return obj.(string)
}
func withExternalReferrer(ctx context.Context, token string) context.Context {
return context.WithValue(ctx, externalReferrerKey, token)
}
func getExternalReferrer(ctx context.Context) string {
obj := ctx.Value(externalReferrerKey)
if obj == nil {
return ""
}
return obj.(string)
}
// withAdminUser adds the admin user to the context.
func withAdminUser(ctx context.Context, u *models.User) context.Context {
return context.WithValue(ctx, adminUserKey, u)
}
// getAdminUser reads the admin user from the context.
func getAdminUser(ctx context.Context) *models.User {
obj := ctx.Value(adminUserKey)
if obj == nil {
return nil
}
return obj.(*models.User)
}
// withRequestToken adds the request token to the context
func withRequestToken(ctx context.Context, token string) context.Context {
return context.WithValue(ctx, oauthTokenKey, token)
}
func getRequestToken(ctx context.Context) string {
obj := ctx.Value(oauthTokenKey)
if obj == nil {
return ""
}
return obj.(string)
}
func withOAuthVerifier(ctx context.Context, token string) context.Context {
return context.WithValue(ctx, oauthVerifierKey, token)
}
func getOAuthVerifier(ctx context.Context) string {
obj := ctx.Value(oauthVerifierKey)
if obj == nil {
return ""
}
return obj.(string)
}
func withSSOProvider(ctx context.Context, provider *models.SSOProvider) context.Context {
return context.WithValue(ctx, ssoProviderKey, provider)
}
func getSSOProvider(ctx context.Context) *models.SSOProvider {
obj := ctx.Value(ssoProviderKey)
if obj == nil {
return nil
}
return obj.(*models.SSOProvider)
}
func withExternalHost(ctx context.Context, u *url.URL) context.Context {
return context.WithValue(ctx, externalHostKey, u)
}
func getExternalHost(ctx context.Context) *url.URL {
obj := ctx.Value(externalHostKey)
if obj == nil {
return nil
}
return obj.(*url.URL)
}

View File

@ -0,0 +1,95 @@
package api
type ErrorCode = string
const (
// ErrorCodeUnknown should not be used directly, it only indicates a failure in the error handling system in such a way that an error code was not assigned properly.
ErrorCodeUnknown ErrorCode = "unknown"
// ErrorCodeUnexpectedFailure signals an unexpected failure such as a 500 Internal Server Error.
ErrorCodeUnexpectedFailure ErrorCode = "unexpected_failure"
ErrorCodeValidationFailed ErrorCode = "validation_failed"
ErrorCodeBadJSON ErrorCode = "bad_json"
ErrorCodeEmailExists ErrorCode = "email_exists"
ErrorCodePhoneExists ErrorCode = "phone_exists"
ErrorCodeBadJWT ErrorCode = "bad_jwt"
ErrorCodeNotAdmin ErrorCode = "not_admin"
ErrorCodeNoAuthorization ErrorCode = "no_authorization"
ErrorCodeUserNotFound ErrorCode = "user_not_found"
ErrorCodeSessionNotFound ErrorCode = "session_not_found"
ErrorCodeSessionExpired ErrorCode = "session_expired"
ErrorCodeRefreshTokenNotFound ErrorCode = "refresh_token_not_found"
ErrorCodeRefreshTokenAlreadyUsed ErrorCode = "refresh_token_already_used"
ErrorCodeFlowStateNotFound ErrorCode = "flow_state_not_found"
ErrorCodeFlowStateExpired ErrorCode = "flow_state_expired"
ErrorCodeSignupDisabled ErrorCode = "signup_disabled"
ErrorCodeUserBanned ErrorCode = "user_banned"
ErrorCodeProviderEmailNeedsVerification ErrorCode = "provider_email_needs_verification"
ErrorCodeInviteNotFound ErrorCode = "invite_not_found"
ErrorCodeBadOAuthState ErrorCode = "bad_oauth_state"
ErrorCodeBadOAuthCallback ErrorCode = "bad_oauth_callback"
ErrorCodeOAuthProviderNotSupported ErrorCode = "oauth_provider_not_supported"
ErrorCodeUnexpectedAudience ErrorCode = "unexpected_audience"
ErrorCodeSingleIdentityNotDeletable ErrorCode = "single_identity_not_deletable"
ErrorCodeEmailConflictIdentityNotDeletable ErrorCode = "email_conflict_identity_not_deletable"
ErrorCodeIdentityAlreadyExists ErrorCode = "identity_already_exists"
ErrorCodeEmailProviderDisabled ErrorCode = "email_provider_disabled"
ErrorCodePhoneProviderDisabled ErrorCode = "phone_provider_disabled"
ErrorCodeTooManyEnrolledMFAFactors ErrorCode = "too_many_enrolled_mfa_factors"
ErrorCodeMFAFactorNameConflict ErrorCode = "mfa_factor_name_conflict"
ErrorCodeMFAFactorNotFound ErrorCode = "mfa_factor_not_found"
ErrorCodeMFAIPAddressMismatch ErrorCode = "mfa_ip_address_mismatch"
ErrorCodeMFAChallengeExpired ErrorCode = "mfa_challenge_expired"
ErrorCodeMFAVerificationFailed ErrorCode = "mfa_verification_failed"
ErrorCodeMFAVerificationRejected ErrorCode = "mfa_verification_rejected"
ErrorCodeInsufficientAAL ErrorCode = "insufficient_aal"
ErrorCodeCaptchaFailed ErrorCode = "captcha_failed"
ErrorCodeSAMLProviderDisabled ErrorCode = "saml_provider_disabled"
ErrorCodeManualLinkingDisabled ErrorCode = "manual_linking_disabled"
ErrorCodeSMSSendFailed ErrorCode = "sms_send_failed"
ErrorCodeEmailNotConfirmed ErrorCode = "email_not_confirmed"
ErrorCodePhoneNotConfirmed ErrorCode = "phone_not_confirmed"
ErrorCodeSAMLRelayStateNotFound ErrorCode = "saml_relay_state_not_found"
ErrorCodeSAMLRelayStateExpired ErrorCode = "saml_relay_state_expired"
ErrorCodeSAMLIdPNotFound ErrorCode = "saml_idp_not_found"
ErrorCodeSAMLAssertionNoUserID ErrorCode = "saml_assertion_no_user_id"
ErrorCodeSAMLAssertionNoEmail ErrorCode = "saml_assertion_no_email"
ErrorCodeUserAlreadyExists ErrorCode = "user_already_exists"
ErrorCodeSSOProviderNotFound ErrorCode = "sso_provider_not_found"
ErrorCodeSAMLMetadataFetchFailed ErrorCode = "saml_metadata_fetch_failed"
ErrorCodeSAMLIdPAlreadyExists ErrorCode = "saml_idp_already_exists"
ErrorCodeSSODomainAlreadyExists ErrorCode = "sso_domain_already_exists"
ErrorCodeSAMLEntityIDMismatch ErrorCode = "saml_entity_id_mismatch"
ErrorCodeConflict ErrorCode = "conflict"
ErrorCodeProviderDisabled ErrorCode = "provider_disabled"
ErrorCodeUserSSOManaged ErrorCode = "user_sso_managed"
ErrorCodeReauthenticationNeeded ErrorCode = "reauthentication_needed"
ErrorCodeSamePassword ErrorCode = "same_password"
ErrorCodeReauthenticationNotValid ErrorCode = "reauthentication_not_valid"
ErrorCodeOTPExpired ErrorCode = "otp_expired"
ErrorCodeOTPDisabled ErrorCode = "otp_disabled"
ErrorCodeIdentityNotFound ErrorCode = "identity_not_found"
ErrorCodeWeakPassword ErrorCode = "weak_password"
ErrorCodeOverRequestRateLimit ErrorCode = "over_request_rate_limit"
ErrorCodeOverEmailSendRateLimit ErrorCode = "over_email_send_rate_limit"
ErrorCodeOverSMSSendRateLimit ErrorCode = "over_sms_send_rate_limit"
ErrorCodeBadCodeVerifier ErrorCode = "bad_code_verifier"
ErrorCodeAnonymousProviderDisabled ErrorCode = "anonymous_provider_disabled"
ErrorCodeHookTimeout ErrorCode = "hook_timeout"
ErrorCodeHookTimeoutAfterRetry ErrorCode = "hook_timeout_after_retry"
ErrorCodeHookPayloadOverSizeLimit ErrorCode = "hook_payload_over_size_limit"
ErrorCodeHookPayloadInvalidContentType ErrorCode = "hook_payload_invalid_content_type"
ErrorCodeRequestTimeout ErrorCode = "request_timeout"
ErrorCodeMFAPhoneEnrollDisabled ErrorCode = "mfa_phone_enroll_not_enabled"
ErrorCodeMFAPhoneVerifyDisabled ErrorCode = "mfa_phone_verify_not_enabled"
ErrorCodeMFATOTPEnrollDisabled ErrorCode = "mfa_totp_enroll_not_enabled"
ErrorCodeMFATOTPVerifyDisabled ErrorCode = "mfa_totp_verify_not_enabled"
ErrorCodeMFAWebAuthnEnrollDisabled ErrorCode = "mfa_webauthn_enroll_not_enabled"
ErrorCodeMFAWebAuthnVerifyDisabled ErrorCode = "mfa_webauthn_verify_not_enabled"
ErrorCodeMFAVerifiedFactorExists ErrorCode = "mfa_verified_factor_exists"
//#nosec G101 -- Not a secret value.
ErrorCodeInvalidCredentials ErrorCode = "invalid_credentials"
ErrorCodeEmailAddressNotAuthorized ErrorCode = "email_address_not_authorized"
ErrorCodeEmailAddressInvalid ErrorCode = "email_address_invalid"
)

View File

@ -0,0 +1,330 @@
package api
import (
"context"
"fmt"
"net/http"
"os"
"runtime/debug"
"time"
"github.com/pkg/errors"
"github.com/supabase/auth/internal/observability"
"github.com/supabase/auth/internal/utilities"
)
// Common error messages during signup flow
var (
DuplicateEmailMsg = "A user with this email address has already been registered"
DuplicatePhoneMsg = "A user with this phone number has already been registered"
UserExistsError error = errors.New("user already exists")
)
const InvalidChannelError = "Invalid channel, supported values are 'sms' or 'whatsapp'. 'whatsapp' is only supported if Twilio or Twilio Verify is used as the provider."
var oauthErrorMap = map[int]string{
http.StatusBadRequest: "invalid_request",
http.StatusUnauthorized: "unauthorized_client",
http.StatusForbidden: "access_denied",
http.StatusInternalServerError: "server_error",
http.StatusServiceUnavailable: "temporarily_unavailable",
}
// OAuthError is the JSON handler for OAuth2 error responses
type OAuthError struct {
Err string `json:"error"`
Description string `json:"error_description,omitempty"`
InternalError error `json:"-"`
InternalMessage string `json:"-"`
}
func (e *OAuthError) Error() string {
if e.InternalMessage != "" {
return e.InternalMessage
}
return fmt.Sprintf("%s: %s", e.Err, e.Description)
}
// WithInternalError adds internal error information to the error
func (e *OAuthError) WithInternalError(err error) *OAuthError {
e.InternalError = err
return e
}
// WithInternalMessage adds internal message information to the error
func (e *OAuthError) WithInternalMessage(fmtString string, args ...interface{}) *OAuthError {
e.InternalMessage = fmt.Sprintf(fmtString, args...)
return e
}
// Cause returns the root cause error
func (e *OAuthError) Cause() error {
if e.InternalError != nil {
return e.InternalError
}
return e
}
func oauthError(err string, description string) *OAuthError {
return &OAuthError{Err: err, Description: description}
}
func badRequestError(errorCode ErrorCode, fmtString string, args ...interface{}) *HTTPError {
return httpError(http.StatusBadRequest, errorCode, fmtString, args...)
}
func internalServerError(fmtString string, args ...interface{}) *HTTPError {
return httpError(http.StatusInternalServerError, ErrorCodeUnexpectedFailure, fmtString, args...)
}
func notFoundError(errorCode ErrorCode, fmtString string, args ...interface{}) *HTTPError {
return httpError(http.StatusNotFound, errorCode, fmtString, args...)
}
func forbiddenError(errorCode ErrorCode, fmtString string, args ...interface{}) *HTTPError {
return httpError(http.StatusForbidden, errorCode, fmtString, args...)
}
func unprocessableEntityError(errorCode ErrorCode, fmtString string, args ...interface{}) *HTTPError {
return httpError(http.StatusUnprocessableEntity, errorCode, fmtString, args...)
}
func tooManyRequestsError(errorCode ErrorCode, fmtString string, args ...interface{}) *HTTPError {
return httpError(http.StatusTooManyRequests, errorCode, fmtString, args...)
}
func conflictError(fmtString string, args ...interface{}) *HTTPError {
return httpError(http.StatusConflict, ErrorCodeConflict, fmtString, args...)
}
// HTTPError is an error with a message and an HTTP status code.
type HTTPError struct {
HTTPStatus int `json:"code"` // do not rename the JSON tags!
ErrorCode string `json:"error_code,omitempty"` // do not rename the JSON tags!
Message string `json:"msg"` // do not rename the JSON tags!
InternalError error `json:"-"`
InternalMessage string `json:"-"`
ErrorID string `json:"error_id,omitempty"`
}
func (e *HTTPError) Error() string {
if e.InternalMessage != "" {
return e.InternalMessage
}
return fmt.Sprintf("%d: %s", e.HTTPStatus, e.Message)
}
func (e *HTTPError) Is(target error) bool {
return e.Error() == target.Error()
}
// Cause returns the root cause error
func (e *HTTPError) Cause() error {
if e.InternalError != nil {
return e.InternalError
}
return e
}
// WithInternalError adds internal error information to the error
func (e *HTTPError) WithInternalError(err error) *HTTPError {
e.InternalError = err
return e
}
// WithInternalMessage adds internal message information to the error
func (e *HTTPError) WithInternalMessage(fmtString string, args ...interface{}) *HTTPError {
e.InternalMessage = fmt.Sprintf(fmtString, args...)
return e
}
func httpError(httpStatus int, errorCode ErrorCode, fmtString string, args ...interface{}) *HTTPError {
return &HTTPError{
HTTPStatus: httpStatus,
ErrorCode: errorCode,
Message: fmt.Sprintf(fmtString, args...),
}
}
// Recoverer is a middleware that recovers from panics, logs the panic (and a
// backtrace), and returns a HTTP 500 (Internal Server Error) status if
// possible. Recoverer prints a request ID if one is provided.
func recoverer(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rvr := recover(); rvr != nil {
logEntry := observability.GetLogEntry(r)
if logEntry != nil {
logEntry.Panic(rvr, debug.Stack())
} else {
fmt.Fprintf(os.Stderr, "Panic: %+v\n", rvr)
debug.PrintStack()
}
se := &HTTPError{
HTTPStatus: http.StatusInternalServerError,
Message: http.StatusText(http.StatusInternalServerError),
}
HandleResponseError(se, w, r)
}
}()
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// ErrorCause is an error interface that contains the method Cause() for returning root cause errors
type ErrorCause interface {
Cause() error
}
type HTTPErrorResponse20240101 struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
}
func HandleResponseError(err error, w http.ResponseWriter, r *http.Request) {
log := observability.GetLogEntry(r).Entry
errorID := utilities.GetRequestID(r.Context())
apiVersion, averr := DetermineClosestAPIVersion(r.Header.Get(APIVersionHeaderName))
if averr != nil {
log.WithError(averr).Warn("Invalid version passed to " + APIVersionHeaderName + " header, defaulting to initial version")
} else if apiVersion != APIVersionInitial {
// Echo back the determined API version from the request
w.Header().Set(APIVersionHeaderName, FormatAPIVersion(apiVersion))
}
switch e := err.(type) {
case *WeakPasswordError:
if apiVersion.Compare(APIVersion20240101) >= 0 {
var output struct {
HTTPErrorResponse20240101
Payload struct {
Reasons []string `json:"reasons,omitempty"`
} `json:"weak_password,omitempty"`
}
output.Code = ErrorCodeWeakPassword
output.Message = e.Message
output.Payload.Reasons = e.Reasons
if jsonErr := sendJSON(w, http.StatusUnprocessableEntity, output); jsonErr != nil && jsonErr != context.DeadlineExceeded {
log.WithError(jsonErr).Warn("Failed to send JSON on ResponseWriter")
}
} else {
var output struct {
HTTPError
Payload struct {
Reasons []string `json:"reasons,omitempty"`
} `json:"weak_password,omitempty"`
}
output.HTTPStatus = http.StatusUnprocessableEntity
output.ErrorCode = ErrorCodeWeakPassword
output.Message = e.Message
output.Payload.Reasons = e.Reasons
w.Header().Set("x-sb-error-code", output.ErrorCode)
if jsonErr := sendJSON(w, output.HTTPStatus, output); jsonErr != nil && jsonErr != context.DeadlineExceeded {
log.WithError(jsonErr).Warn("Failed to send JSON on ResponseWriter")
}
}
case *HTTPError:
switch {
case e.HTTPStatus >= http.StatusInternalServerError:
e.ErrorID = errorID
// this will get us the stack trace too
log.WithError(e.Cause()).Error(e.Error())
case e.HTTPStatus == http.StatusTooManyRequests:
log.WithError(e.Cause()).Warn(e.Error())
default:
log.WithError(e.Cause()).Info(e.Error())
}
if e.ErrorCode != "" {
w.Header().Set("x-sb-error-code", e.ErrorCode)
}
if apiVersion.Compare(APIVersion20240101) >= 0 {
resp := HTTPErrorResponse20240101{
Code: e.ErrorCode,
Message: e.Message,
}
if resp.Code == "" {
if e.HTTPStatus == http.StatusInternalServerError {
resp.Code = ErrorCodeUnexpectedFailure
} else {
resp.Code = ErrorCodeUnknown
}
}
if jsonErr := sendJSON(w, e.HTTPStatus, resp); jsonErr != nil && jsonErr != context.DeadlineExceeded {
log.WithError(jsonErr).Warn("Failed to send JSON on ResponseWriter")
}
} else {
if e.ErrorCode == "" {
if e.HTTPStatus == http.StatusInternalServerError {
e.ErrorCode = ErrorCodeUnexpectedFailure
} else {
e.ErrorCode = ErrorCodeUnknown
}
}
// Provide better error messages for certain user-triggered Postgres errors.
if pgErr := utilities.NewPostgresError(e.InternalError); pgErr != nil {
if jsonErr := sendJSON(w, pgErr.HttpStatusCode, pgErr); jsonErr != nil && jsonErr != context.DeadlineExceeded {
log.WithError(jsonErr).Warn("Failed to send JSON on ResponseWriter")
}
return
}
if jsonErr := sendJSON(w, e.HTTPStatus, e); jsonErr != nil && jsonErr != context.DeadlineExceeded {
log.WithError(jsonErr).Warn("Failed to send JSON on ResponseWriter")
}
}
case *OAuthError:
log.WithError(e.Cause()).Info(e.Error())
if jsonErr := sendJSON(w, http.StatusBadRequest, e); jsonErr != nil && jsonErr != context.DeadlineExceeded {
log.WithError(jsonErr).Warn("Failed to send JSON on ResponseWriter")
}
case ErrorCause:
HandleResponseError(e.Cause(), w, r)
default:
log.WithError(e).Errorf("Unhandled server error: %s", e.Error())
if apiVersion.Compare(APIVersion20240101) >= 0 {
resp := HTTPErrorResponse20240101{
Code: ErrorCodeUnexpectedFailure,
Message: "Unexpected failure, please check server logs for more information",
}
if jsonErr := sendJSON(w, http.StatusInternalServerError, resp); jsonErr != nil && jsonErr != context.DeadlineExceeded {
log.WithError(jsonErr).Warn("Failed to send JSON on ResponseWriter")
}
} else {
httpError := HTTPError{
HTTPStatus: http.StatusInternalServerError,
ErrorCode: ErrorCodeUnexpectedFailure,
Message: "Unexpected failure, please check server logs for more information",
}
if jsonErr := sendJSON(w, http.StatusInternalServerError, httpError); jsonErr != nil && jsonErr != context.DeadlineExceeded {
log.WithError(jsonErr).Warn("Failed to send JSON on ResponseWriter")
}
}
}
}
func generateFrequencyLimitErrorMessage(timeStamp *time.Time, maxFrequency time.Duration) string {
now := time.Now()
left := timeStamp.Add(maxFrequency).Sub(now) / time.Second
return fmt.Sprintf("For security purposes, you can only request this after %d seconds.", left)
}

View File

@ -0,0 +1,105 @@
package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/observability"
)
func TestHandleResponseErrorWithHTTPError(t *testing.T) {
examples := []struct {
HTTPError *HTTPError
APIVersion string
ExpectedBody string
}{
{
HTTPError: badRequestError(ErrorCodeBadJSON, "Unable to parse JSON"),
APIVersion: "",
ExpectedBody: "{\"code\":400,\"error_code\":\"" + ErrorCodeBadJSON + "\",\"msg\":\"Unable to parse JSON\"}",
},
{
HTTPError: badRequestError(ErrorCodeBadJSON, "Unable to parse JSON"),
APIVersion: "2023-12-31",
ExpectedBody: "{\"code\":400,\"error_code\":\"" + ErrorCodeBadJSON + "\",\"msg\":\"Unable to parse JSON\"}",
},
{
HTTPError: badRequestError(ErrorCodeBadJSON, "Unable to parse JSON"),
APIVersion: "2024-01-01",
ExpectedBody: "{\"code\":\"" + ErrorCodeBadJSON + "\",\"message\":\"Unable to parse JSON\"}",
},
{
HTTPError: &HTTPError{
HTTPStatus: http.StatusBadRequest,
Message: "Uncoded failure",
},
APIVersion: "2024-01-01",
ExpectedBody: "{\"code\":\"" + ErrorCodeUnknown + "\",\"message\":\"Uncoded failure\"}",
},
{
HTTPError: &HTTPError{
HTTPStatus: http.StatusInternalServerError,
Message: "Unexpected failure",
},
APIVersion: "2024-01-01",
ExpectedBody: "{\"code\":\"" + ErrorCodeUnexpectedFailure + "\",\"message\":\"Unexpected failure\"}",
},
}
for _, example := range examples {
rec := httptest.NewRecorder()
req, err := http.NewRequest(http.MethodPost, "http://example.com", nil)
require.NoError(t, err)
if example.APIVersion != "" {
req.Header.Set(APIVersionHeaderName, example.APIVersion)
}
HandleResponseError(example.HTTPError, rec, req)
require.Equal(t, example.HTTPError.HTTPStatus, rec.Code)
require.Equal(t, example.ExpectedBody, rec.Body.String())
}
}
func TestRecoverer(t *testing.T) {
var logBuffer bytes.Buffer
config, err := conf.LoadGlobal(apiTestConfig)
require.NoError(t, err)
require.NoError(t, observability.ConfigureLogging(&config.Logging))
// logrus should write to the buffer so we can check if the logs are output correctly
logrus.SetOutput(&logBuffer)
panicHandler := recoverer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
panic("test panic")
}))
w := httptest.NewRecorder()
req, err := http.NewRequest(http.MethodPost, "http://example.com", nil)
require.NoError(t, err)
panicHandler.ServeHTTP(w, req)
require.Equal(t, http.StatusInternalServerError, w.Code)
var data HTTPError
// panic should return an internal server error
require.NoError(t, json.NewDecoder(w.Body).Decode(&data))
require.Equal(t, ErrorCodeUnexpectedFailure, data.ErrorCode)
require.Equal(t, http.StatusInternalServerError, data.HTTPStatus)
require.Equal(t, "Internal Server Error", data.Message)
// panic should log the error message internally
var logs map[string]interface{}
require.NoError(t, json.NewDecoder(&logBuffer).Decode(&logs))
require.Equal(t, "request panicked", logs["msg"])
require.Equal(t, "test panic", logs["panic"])
require.NotEmpty(t, logs["stack"])
}

View File

@ -0,0 +1,684 @@
package api
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/fatih/structs"
"github.com/gofrs/uuid"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/sirupsen/logrus"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/observability"
"github.com/supabase/auth/internal/storage"
"github.com/supabase/auth/internal/utilities"
"golang.org/x/oauth2"
)
// ExternalProviderClaims are the JWT claims sent as the state in the external oauth provider signup flow
type ExternalProviderClaims struct {
AuthMicroserviceClaims
Provider string `json:"provider"`
InviteToken string `json:"invite_token,omitempty"`
Referrer string `json:"referrer,omitempty"`
FlowStateID string `json:"flow_state_id"`
LinkingTargetID string `json:"linking_target_id,omitempty"`
}
// ExternalProviderRedirect redirects the request to the oauth provider
func (a *API) ExternalProviderRedirect(w http.ResponseWriter, r *http.Request) error {
rurl, err := a.GetExternalProviderRedirectURL(w, r, nil)
if err != nil {
return err
}
http.Redirect(w, r, rurl, http.StatusFound)
return nil
}
// GetExternalProviderRedirectURL returns the URL to start the oauth flow with the corresponding oauth provider
func (a *API) GetExternalProviderRedirectURL(w http.ResponseWriter, r *http.Request, linkingTargetUser *models.User) (string, error) {
ctx := r.Context()
db := a.db.WithContext(ctx)
config := a.config
query := r.URL.Query()
providerType := query.Get("provider")
scopes := query.Get("scopes")
codeChallenge := query.Get("code_challenge")
codeChallengeMethod := query.Get("code_challenge_method")
p, err := a.Provider(ctx, providerType, scopes)
if err != nil {
return "", badRequestError(ErrorCodeValidationFailed, "Unsupported provider: %+v", err).WithInternalError(err)
}
inviteToken := query.Get("invite_token")
if inviteToken != "" {
_, userErr := models.FindUserByConfirmationToken(db, inviteToken)
if userErr != nil {
if models.IsNotFoundError(userErr) {
return "", notFoundError(ErrorCodeUserNotFound, "User identified by token not found")
}
return "", internalServerError("Database error finding user").WithInternalError(userErr)
}
}
redirectURL := utilities.GetReferrer(r, config)
log := observability.GetLogEntry(r).Entry
log.WithField("provider", providerType).Info("Redirecting to external provider")
if err := validatePKCEParams(codeChallengeMethod, codeChallenge); err != nil {
return "", err
}
flowType := getFlowFromChallenge(codeChallenge)
flowStateID := ""
if isPKCEFlow(flowType) {
flowState, err := generateFlowState(a.db, providerType, models.OAuth, codeChallengeMethod, codeChallenge, nil)
if err != nil {
return "", err
}
flowStateID = flowState.ID.String()
}
claims := ExternalProviderClaims{
AuthMicroserviceClaims: AuthMicroserviceClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),
},
SiteURL: config.SiteURL,
InstanceID: uuid.Nil.String(),
},
Provider: providerType,
InviteToken: inviteToken,
Referrer: redirectURL,
FlowStateID: flowStateID,
}
if linkingTargetUser != nil {
// this means that the user is performing manual linking
claims.LinkingTargetID = linkingTargetUser.ID.String()
}
tokenString, err := signJwt(&config.JWT, claims)
if err != nil {
return "", internalServerError("Error creating state").WithInternalError(err)
}
authUrlParams := make([]oauth2.AuthCodeOption, 0)
query.Del("scopes")
query.Del("provider")
query.Del("code_challenge")
query.Del("code_challenge_method")
for key := range query {
if key == "workos_provider" {
// See https://workos.com/docs/reference/sso/authorize/get
authUrlParams = append(authUrlParams, oauth2.SetAuthURLParam("provider", query.Get(key)))
} else {
authUrlParams = append(authUrlParams, oauth2.SetAuthURLParam(key, query.Get(key)))
}
}
authURL := p.AuthCodeURL(tokenString, authUrlParams...)
return authURL, nil
}
// ExternalProviderCallback handles the callback endpoint in the external oauth provider flow
func (a *API) ExternalProviderCallback(w http.ResponseWriter, r *http.Request) error {
rurl := a.getExternalRedirectURL(r)
u, err := url.Parse(rurl)
if err != nil {
return err
}
redirectErrors(a.internalExternalProviderCallback, w, r, u)
return nil
}
func (a *API) handleOAuthCallback(r *http.Request) (*OAuthProviderData, error) {
ctx := r.Context()
providerType := getExternalProviderType(ctx)
var oAuthResponseData *OAuthProviderData
var err error
switch providerType {
case "twitter":
// future OAuth1.0 providers will use this method
oAuthResponseData, err = a.oAuth1Callback(ctx, providerType)
default:
oAuthResponseData, err = a.oAuthCallback(ctx, r, providerType)
}
if err != nil {
return nil, err
}
return oAuthResponseData, nil
}
func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)
var grantParams models.GrantParams
grantParams.FillGrantParams(r)
providerType := getExternalProviderType(ctx)
data, err := a.handleOAuthCallback(r)
if err != nil {
return err
}
userData := data.userData
if len(userData.Emails) <= 0 {
return internalServerError("Error getting user email from external provider")
}
userData.Metadata.EmailVerified = false
for _, email := range userData.Emails {
if email.Primary {
userData.Metadata.Email = email.Email
userData.Metadata.EmailVerified = email.Verified
break
} else {
userData.Metadata.Email = email.Email
userData.Metadata.EmailVerified = email.Verified
}
}
providerAccessToken := data.token
providerRefreshToken := data.refreshToken
var flowState *models.FlowState
// if there's a non-empty FlowStateID we perform PKCE Flow
if flowStateID := getFlowStateID(ctx); flowStateID != "" {
flowState, err = models.FindFlowStateByID(a.db, flowStateID)
if models.IsNotFoundError(err) {
return unprocessableEntityError(ErrorCodeFlowStateNotFound, "Flow state not found").WithInternalError(err)
} else if err != nil {
return internalServerError("Failed to find flow state").WithInternalError(err)
}
}
var user *models.User
var token *AccessTokenResponse
err = db.Transaction(func(tx *storage.Connection) error {
var terr error
if targetUser := getTargetUser(ctx); targetUser != nil {
if user, terr = a.linkIdentityToUser(r, ctx, tx, userData, providerType); terr != nil {
return terr
}
} else if inviteToken := getInviteToken(ctx); inviteToken != "" {
if user, terr = a.processInvite(r, tx, userData, inviteToken, providerType); terr != nil {
return terr
}
} else {
if user, terr = a.createAccountFromExternalIdentity(tx, r, userData, providerType); terr != nil {
return terr
}
}
if flowState != nil {
// This means that the callback is using PKCE
flowState.ProviderAccessToken = providerAccessToken
flowState.ProviderRefreshToken = providerRefreshToken
flowState.UserID = &(user.ID)
issueTime := time.Now()
flowState.AuthCodeIssuedAt = &issueTime
terr = tx.Update(flowState)
} else {
token, terr = a.issueRefreshToken(r, tx, user, models.OAuth, grantParams)
}
if terr != nil {
return oauthError("server_error", terr.Error())
}
return nil
})
if err != nil {
return err
}
rurl := a.getExternalRedirectURL(r)
if flowState != nil {
// This means that the callback is using PKCE
// Set the flowState.AuthCode to the query param here
rurl, err = a.prepPKCERedirectURL(rurl, flowState.AuthCode)
if err != nil {
return err
}
} else if token != nil {
q := url.Values{}
q.Set("provider_token", providerAccessToken)
// Because not all providers give out a refresh token
// See corresponding OAuth2 spec: <https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1>
if providerRefreshToken != "" {
q.Set("provider_refresh_token", providerRefreshToken)
}
rurl = token.AsRedirectURL(rurl, q)
}
http.Redirect(w, r, rurl, http.StatusFound)
return nil
}
func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http.Request, userData *provider.UserProvidedData, providerType string) (*models.User, error) {
ctx := r.Context()
aud := a.requestAud(ctx, r)
config := a.config
var user *models.User
var identity *models.Identity
var identityData map[string]interface{}
if userData.Metadata != nil {
identityData = structs.Map(userData.Metadata)
}
decision, terr := models.DetermineAccountLinking(tx, config, userData.Emails, aud, providerType, userData.Metadata.Subject)
if terr != nil {
return nil, terr
}
switch decision.Decision {
case models.LinkAccount:
user = decision.User
if identity, terr = a.createNewIdentity(tx, user, providerType, identityData); terr != nil {
return nil, terr
}
if terr = user.UpdateUserMetaData(tx, identityData); terr != nil {
return nil, terr
}
if terr = user.UpdateAppMetaDataProviders(tx); terr != nil {
return nil, terr
}
case models.CreateAccount:
if config.DisableSignup {
return nil, unprocessableEntityError(ErrorCodeSignupDisabled, "Signups not allowed for this instance")
}
params := &SignupParams{
Provider: providerType,
Email: decision.CandidateEmail.Email,
Aud: aud,
Data: identityData,
}
isSSOUser := false
if strings.HasPrefix(decision.LinkingDomain, "sso:") {
isSSOUser = true
}
// because params above sets no password, this method is not
// computationally hard so it can be used within a database
// transaction
user, terr = params.ToUserModel(isSSOUser)
if terr != nil {
return nil, terr
}
if user, terr = a.signupNewUser(tx, user); terr != nil {
return nil, terr
}
if identity, terr = a.createNewIdentity(tx, user, providerType, identityData); terr != nil {
return nil, terr
}
user.Identities = append(user.Identities, *identity)
case models.AccountExists:
user = decision.User
identity = decision.Identities[0]
identity.IdentityData = identityData
if terr = tx.UpdateOnly(identity, "identity_data", "last_sign_in_at"); terr != nil {
return nil, terr
}
if terr = user.UpdateUserMetaData(tx, identityData); terr != nil {
return nil, terr
}
if terr = user.UpdateAppMetaDataProviders(tx); terr != nil {
return nil, terr
}
case models.MultipleAccounts:
return nil, internalServerError("Multiple accounts with the same email address in the same linking domain detected: %v", decision.LinkingDomain)
default:
return nil, internalServerError("Unknown automatic linking decision: %v", decision.Decision)
}
if user.IsBanned() {
return nil, forbiddenError(ErrorCodeUserBanned, "User is banned")
}
if !user.IsConfirmed() {
// The user may have other unconfirmed email + password
// combination, phone or oauth identities. These identities
// need to be removed when a new oauth identity is being added
// to prevent pre-account takeover attacks from happening.
if terr = user.RemoveUnconfirmedIdentities(tx, identity); terr != nil {
return nil, internalServerError("Error updating user").WithInternalError(terr)
}
if decision.CandidateEmail.Verified || config.Mailer.Autoconfirm {
if terr := models.NewAuditLogEntry(r, tx, user, models.UserSignedUpAction, "", map[string]interface{}{
"provider": providerType,
}); terr != nil {
return nil, terr
}
// fall through to auto-confirm and issue token
if terr = user.Confirm(tx); terr != nil {
return nil, internalServerError("Error updating user").WithInternalError(terr)
}
} else {
emailConfirmationSent := false
if decision.CandidateEmail.Email != "" {
if terr = a.sendConfirmation(r, tx, user, models.ImplicitFlow); terr != nil {
return nil, terr
}
emailConfirmationSent = true
}
if !config.Mailer.AllowUnverifiedEmailSignIns {
if emailConfirmationSent {
return nil, storage.NewCommitWithError(unprocessableEntityError(ErrorCodeProviderEmailNeedsVerification, fmt.Sprintf("Unverified email with %v. A confirmation email has been sent to your %v email", providerType, providerType)))
}
return nil, storage.NewCommitWithError(unprocessableEntityError(ErrorCodeProviderEmailNeedsVerification, fmt.Sprintf("Unverified email with %v. Verify the email with %v in order to sign in", providerType, providerType)))
}
}
} else {
if terr := models.NewAuditLogEntry(r, tx, user, models.LoginAction, "", map[string]interface{}{
"provider": providerType,
}); terr != nil {
return nil, terr
}
}
return user, nil
}
func (a *API) processInvite(r *http.Request, tx *storage.Connection, userData *provider.UserProvidedData, inviteToken, providerType string) (*models.User, error) {
user, err := models.FindUserByConfirmationToken(tx, inviteToken)
if err != nil {
if models.IsNotFoundError(err) {
return nil, notFoundError(ErrorCodeInviteNotFound, "Invite not found")
}
return nil, internalServerError("Database error finding user").WithInternalError(err)
}
var emailData *provider.Email
var emails []string
for i, e := range userData.Emails {
emails = append(emails, e.Email)
if user.GetEmail() == e.Email {
emailData = &userData.Emails[i]
break
}
}
if emailData == nil {
return nil, badRequestError(ErrorCodeValidationFailed, "Invited email does not match emails from external provider").WithInternalMessage("invited=%s external=%s", user.Email, strings.Join(emails, ", "))
}
var identityData map[string]interface{}
if userData.Metadata != nil {
identityData = structs.Map(userData.Metadata)
}
identity, err := a.createNewIdentity(tx, user, providerType, identityData)
if err != nil {
return nil, err
}
if err := user.UpdateAppMetaData(tx, map[string]interface{}{
"provider": providerType,
}); err != nil {
return nil, err
}
if err := user.UpdateAppMetaDataProviders(tx); err != nil {
return nil, err
}
if err := user.UpdateUserMetaData(tx, identityData); err != nil {
return nil, internalServerError("Database error updating user").WithInternalError(err)
}
if err := models.NewAuditLogEntry(r, tx, user, models.InviteAcceptedAction, "", map[string]interface{}{
"provider": providerType,
}); err != nil {
return nil, err
}
// an account with a previously unconfirmed email + password
// combination or phone may exist. so now that there is an
// OAuth identity bound to this user, and since they have not
// confirmed their email or phone, they are unaware that a
// potentially malicious door exists into their account; thus
// the password and phone needs to be removed.
if err := user.RemoveUnconfirmedIdentities(tx, identity); err != nil {
return nil, internalServerError("Error updating user").WithInternalError(err)
}
// confirm because they were able to respond to invite email
if err := user.Confirm(tx); err != nil {
return nil, err
}
return user, nil
}
func (a *API) loadExternalState(ctx context.Context, r *http.Request) (context.Context, error) {
var state string
switch r.Method {
case http.MethodPost:
state = r.FormValue("state")
default:
state = r.URL.Query().Get("state")
}
if state == "" {
return ctx, badRequestError(ErrorCodeBadOAuthCallback, "OAuth state parameter missing")
}
config := a.config
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods(config.JWT.ValidMethods))
_, err := p.ParseWithClaims(state, &claims, func(token *jwt.Token) (interface{}, error) {
if kid, ok := token.Header["kid"]; ok {
if kidStr, ok := kid.(string); ok {
return conf.FindPublicKeyByKid(kidStr, &config.JWT)
}
}
if alg, ok := token.Header["alg"]; ok {
if alg == jwt.SigningMethodHS256.Name {
// preserve backward compatibility for cases where the kid is not set
return []byte(config.JWT.Secret), nil
}
}
return nil, fmt.Errorf("missing kid")
})
if err != nil {
return ctx, badRequestError(ErrorCodeBadOAuthState, "OAuth callback with invalid state").WithInternalError(err)
}
if claims.Provider == "" {
return ctx, badRequestError(ErrorCodeBadOAuthState, "OAuth callback with invalid state (missing provider)")
}
if claims.InviteToken != "" {
ctx = withInviteToken(ctx, claims.InviteToken)
}
if claims.Referrer != "" {
ctx = withExternalReferrer(ctx, claims.Referrer)
}
if claims.FlowStateID != "" {
ctx = withFlowStateID(ctx, claims.FlowStateID)
}
if claims.LinkingTargetID != "" {
linkingTargetUserID, err := uuid.FromString(claims.LinkingTargetID)
if err != nil {
return nil, badRequestError(ErrorCodeBadOAuthState, "OAuth callback with invalid state (linking_target_id must be UUID)")
}
u, err := models.FindUserByID(a.db, linkingTargetUserID)
if err != nil {
if models.IsNotFoundError(err) {
return nil, unprocessableEntityError(ErrorCodeUserNotFound, "Linking target user not found")
}
return nil, internalServerError("Database error loading user").WithInternalError(err)
}
ctx = withTargetUser(ctx, u)
}
ctx = withExternalProviderType(ctx, claims.Provider)
return withSignature(ctx, state), nil
}
// Provider returns a Provider interface for the given name.
func (a *API) Provider(ctx context.Context, name string, scopes string) (provider.Provider, error) {
config := a.config
name = strings.ToLower(name)
switch name {
case "apple":
return provider.NewAppleProvider(ctx, config.External.Apple)
case "azure":
return provider.NewAzureProvider(config.External.Azure, scopes)
case "bitbucket":
return provider.NewBitbucketProvider(config.External.Bitbucket)
case "discord":
return provider.NewDiscordProvider(config.External.Discord, scopes)
case "facebook":
return provider.NewFacebookProvider(config.External.Facebook, scopes)
case "figma":
return provider.NewFigmaProvider(config.External.Figma, scopes)
case "fly":
return provider.NewFlyProvider(config.External.Fly, scopes)
case "github":
return provider.NewGithubProvider(config.External.Github, scopes)
case "gitlab":
return provider.NewGitlabProvider(config.External.Gitlab, scopes)
case "google":
return provider.NewGoogleProvider(ctx, config.External.Google, scopes)
case "kakao":
return provider.NewKakaoProvider(config.External.Kakao, scopes)
case "keycloak":
return provider.NewKeycloakProvider(config.External.Keycloak, scopes)
case "linkedin":
return provider.NewLinkedinProvider(config.External.Linkedin, scopes)
case "linkedin_oidc":
return provider.NewLinkedinOIDCProvider(config.External.LinkedinOIDC, scopes)
case "notion":
return provider.NewNotionProvider(config.External.Notion)
case "spotify":
return provider.NewSpotifyProvider(config.External.Spotify, scopes)
case "slack":
return provider.NewSlackProvider(config.External.Slack, scopes)
case "slack_oidc":
return provider.NewSlackOIDCProvider(config.External.SlackOIDC, scopes)
case "twitch":
return provider.NewTwitchProvider(config.External.Twitch, scopes)
case "twitter":
return provider.NewTwitterProvider(config.External.Twitter, scopes)
case "vercel_marketplace":
return provider.NewVercelMarketplaceProvider(config.External.VercelMarketplace, scopes)
case "workos":
return provider.NewWorkOSProvider(config.External.WorkOS)
case "zoom":
return provider.NewZoomProvider(config.External.Zoom)
default:
return nil, fmt.Errorf("Provider %s could not be found", name)
}
}
func redirectErrors(handler apiHandler, w http.ResponseWriter, r *http.Request, u *url.URL) {
ctx := r.Context()
log := observability.GetLogEntry(r).Entry
errorID := utilities.GetRequestID(ctx)
err := handler(w, r)
if err != nil {
q := getErrorQueryString(err, errorID, log, u.Query())
u.RawQuery = q.Encode()
// TODO: deprecate returning error details in the query fragment
hq := url.Values{}
if q.Get("error") != "" {
hq.Set("error", q.Get("error"))
}
if q.Get("error_description") != "" {
hq.Set("error_description", q.Get("error_description"))
}
if q.Get("error_code") != "" {
hq.Set("error_code", q.Get("error_code"))
}
u.Fragment = hq.Encode()
http.Redirect(w, r, u.String(), http.StatusFound)
}
}
func getErrorQueryString(err error, errorID string, log logrus.FieldLogger, q url.Values) *url.Values {
switch e := err.(type) {
case *HTTPError:
if e.ErrorCode == ErrorCodeSignupDisabled {
q.Set("error", "access_denied")
} else if e.ErrorCode == ErrorCodeUserBanned {
q.Set("error", "access_denied")
} else if e.ErrorCode == ErrorCodeProviderEmailNeedsVerification {
q.Set("error", "access_denied")
} else if str, ok := oauthErrorMap[e.HTTPStatus]; ok {
q.Set("error", str)
} else {
q.Set("error", "server_error")
}
if e.HTTPStatus >= http.StatusInternalServerError {
e.ErrorID = errorID
// this will get us the stack trace too
log.WithError(e.Cause()).Error(e.Error())
} else {
log.WithError(e.Cause()).Info(e.Error())
}
q.Set("error_description", e.Message)
q.Set("error_code", e.ErrorCode)
case *OAuthError:
q.Set("error", e.Err)
q.Set("error_description", e.Description)
log.WithError(e.Cause()).Info(e.Error())
case ErrorCause:
return getErrorQueryString(e.Cause(), errorID, log, q)
default:
error_type, error_description := "server_error", err.Error()
// Provide better error messages for certain user-triggered Postgres errors.
if pgErr := utilities.NewPostgresError(e); pgErr != nil {
error_description = pgErr.Message
if oauthErrorType, ok := oauthErrorMap[pgErr.HttpStatusCode]; ok {
error_type = oauthErrorType
}
}
q.Set("error", error_type)
q.Set("error_description", error_description)
}
return &q
}
func (a *API) getExternalRedirectURL(r *http.Request) string {
ctx := r.Context()
config := a.config
if config.External.RedirectURL != "" {
return config.External.RedirectURL
}
if er := getExternalReferrer(ctx); er != "" {
return er
}
return config.SiteURL
}
func (a *API) createNewIdentity(tx *storage.Connection, user *models.User, providerType string, identityData map[string]interface{}) (*models.Identity, error) {
identity, err := models.NewIdentity(user, providerType, identityData)
if err != nil {
return nil, err
}
if terr := tx.Create(identity); terr != nil {
return nil, internalServerError("Error creating identity").WithInternalError(terr)
}
return identity, nil
}

View File

@ -0,0 +1,33 @@
package api
import (
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
)
func (ts *ExternalTestSuite) TestSignupExternalApple() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=apple", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Apple.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Apple.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("email name", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("apple", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}

View File

@ -0,0 +1,269 @@
package api
import (
"context"
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"time"
"github.com/coreos/go-oidc/v3/oidc"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/supabase/auth/internal/api/provider"
)
const (
azureUser string = `{"name":"Azure Test","email":"azure@example.com","sub":"azuretestid"}`
azureUserNoEmail string = `{"name":"Azure Test","sub":"azuretestid"}`
)
func idTokenPrivateKey() *rsa.PrivateKey {
// #nosec
der, err := base64.StdEncoding.DecodeString("MIIEpAIBAAKCAQEAvklrFDsVgbhs3DOQICMqm4xdFoi/MHj/T6XH8S7wXWd0roqdWVarwCLV4y3DILkLre4PzNK+hEY5NAnoAKrsCMyyCb4Wdl8HCdJk4ojDqAig+DJw67imqZoxJMFJyIhfMJhwVK1V8GRUPATn855rygLo7wThahMJeEHNiJr3TtV6Rf35KSs7DuyoWIUSjISYabQozKqIvpdUpTpSqjlOQvjdAxggRyycBZSgLzjWhsA8metnAMO48bX4bgiHLR6Kzu/dfPyEVPfgeYpA2ebIY6GzIUxVS0yX8+ExA6jeLCkuepjLHuz5XCJtd6zzGDXr1eX7nA6ZIeUNdFbWRDnPawIDAQABAoIBABH4Qvl1HvHSJc2hvPGcAJER71SKc2uzcYDnCfu30BEyDO3Sv0tJiQyq/YHnt26mqviw66MPH9jD/PDyIou1mHa4RfPvlJV3IeYGjWprOfbrYbAuq0VHec24dv2el0YtwreHHcyRVfVOtDm6yODTzCAWqEKyNktbIuDNbgiBgetayaJecDRoFMF9TOCeMCL92iZytzAr7fi+JWtLkRS/GZRIBjbr8LJ/ueYoCRmIx3MIw0WdPp7v2ZfeRTxP7LxJZ+MAsrq2pstmZYP7K0305e0bCJX1HexfXLs2Ul7u8zaxrXL8zw4/9+/GMsAeU3ffCVnGz/RKL5+T6iuz2RotjFECgYEA+Xk7DGwRXfDg9xba1GVFGeiC4nybqZw/RfZKcz/RRJWSHRJV/ps1avtbca3B19rjI6rewZMO1NWNv/tI2BdXP8vAKUnI9OHJZ+J/eZzmqDE6qu0v0ddRFUDzCMWE0j8BjrUdy44n4NQgopcv14u0iyr9tuhGO6YXn2SuuvEkZokCgYEAw0PNnT55kpkEhXSp7An2hdBJEub9ST7hS6Kcd8let62/qUZ/t5jWigSkWC1A2bMtH55+LgudIFjiehwVzRs7jym2j4jkKZGonyAX1l9IWgXwKl7Pn49lEQH5Yk6MhnXdyLGoFTzXiUyk/fKvgXX7jow1bD3j6sAc8P495I7TyVMCgYAHg6VJrH+har37805IE3zPWPeIRuSRaUlmnBKGAigVfsPV6FV6w8YKIOQSOn+aNtecnWr0Pa+2rXAFllYNXDaej06Mb9KDvcFJRcM9MIKqEkGIIHjOQ0QH9drcKsbjZk5vs/jfxrpgxULuYstoHKclgff+aGSlK02O2YOB0f2csQKBgQCEC/MdNiWCpKXxFg7fB3HF1i/Eb56zjKlQu7uyKeQ6tG3bLEisQNg8Z5034Apt7gRC0KyluMbeHB2z1BBOLu9dBill8X3SOqVcTpiwKKlF76QVEx622YLQOJSMDXBscYK0+KchDY74U3N0JEzZcI7YPCrYcxYRJy+rLVNvn8LK7wKBgQDE8THsZ589e10F0zDBvPK56o8PJnPeH71sgdM2Co4oLzBJ6g0rpJOKfcc03fLHsoJVOAya9WZeIy6K8+WVdcPTadR07S4p8/tcK1eguu5qlmCUOzswrTKAaJoIHO7cddQp3nySIqgYtkGdHKuvlQDMQkEKJS0meOm+vdeAG2rkaA==")
if err != nil {
panic(err)
}
privateKey, err := x509.ParsePKCS1PrivateKey(der)
if err != nil {
panic(err)
}
privateKey.E = 65537
return privateKey
}
func setupAzureOverrideVerifiers() {
provider.OverrideVerifiers["https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/oauth2/v2.0/authorize"] = func(ctx context.Context, config *oidc.Config) *oidc.IDTokenVerifier {
pk := idTokenPrivateKey()
return oidc.NewVerifier(
provider.IssuerAzureMicrosoft,
&oidc.StaticKeySet{
PublicKeys: []crypto.PublicKey{
&pk.PublicKey,
},
},
config,
)
}
}
func mintIDToken(user string) string {
var idToken struct {
Issuer string `json:"iss"`
IssuedAt int `json:"iat"`
ExpiresAt int `json:"exp"`
Audience string `json:"aud"`
Sub string `json:"sub,omitempty"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
XmsEdov any `json:"xms_edov,omitempty"`
}
if err := json.Unmarshal([]byte(user), &idToken); err != nil {
panic(err)
}
now := time.Now()
idToken.Issuer = provider.IssuerAzureMicrosoft
idToken.IssuedAt = int(now.Unix())
idToken.ExpiresAt = int(now.Unix() + 60*60)
idToken.Audience = "testclientid"
header := base64.RawURLEncoding.EncodeToString([]byte(`{"typ":"JWT","alg":"RS256"}`))
data, err := json.Marshal(idToken)
if err != nil {
panic(err)
}
payload := base64.RawURLEncoding.EncodeToString(data)
sum := sha256.Sum256([]byte(header + "." + payload))
pk := idTokenPrivateKey()
sig, err := rsa.SignPKCS1v15(nil, pk, crypto.SHA256, sum[:])
if err != nil {
panic(err)
}
token := header + "." + payload + "." + base64.RawURLEncoding.EncodeToString(sig)
return token
}
func (ts *ExternalTestSuite) TestSignupExternalAzure() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=azure", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Azure.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Azure.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("openid", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("azure", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func AzureTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, code string, user string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/oauth2/v2.0/token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Azure.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, `{"access_token":"azure_token","expires_in":100000,"id_token":%q}`, mintIDToken(user))
default:
w.WriteHeader(500)
ts.Fail("unknown azure oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Azure.URL = server.URL
ts.Config.External.Azure.ApiURL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalAzure_AuthorizationCode() {
setupAzureOverrideVerifiers()
ts.Config.DisableSignup = false
tokenCount := 0
code := "authcode"
server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser)
defer server.Close()
u := performAuthorization(ts, "azure", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, -1, "azure@example.com", "Azure Test", "azuretestid", "")
}
func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupErrorWhenNoUser() {
setupAzureOverrideVerifiers()
ts.Config.DisableSignup = true
tokenCount := 0
code := "authcode"
server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser)
defer server.Close()
u := performAuthorization(ts, "azure", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "azure@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupErrorWhenNoEmail() {
setupAzureOverrideVerifiers()
ts.Config.DisableSignup = true
tokenCount := 0
code := "authcode"
server := AzureTestSignupSetup(ts, &tokenCount, code, azureUserNoEmail)
defer server.Close()
u := performAuthorization(ts, "azure", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "azure@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupSuccessWithPrimaryEmail() {
setupAzureOverrideVerifiers()
ts.Config.DisableSignup = true
ts.createUser("azuretestid", "azure@example.com", "Azure Test", "", "")
tokenCount := 0
code := "authcode"
server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser)
defer server.Close()
u := performAuthorization(ts, "azure", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, -1, "azure@example.com", "Azure Test", "azuretestid", "")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalAzureSuccessWhenMatchingToken() {
setupAzureOverrideVerifiers()
// name should be populated from Azure API
ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token")
tokenCount := 0
code := "authcode"
server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser)
defer server.Close()
u := performAuthorization(ts, "azure", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, -1, "azure@example.com", "Azure Test", "azuretestid", "")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenNoMatchingToken() {
setupAzureOverrideVerifiers()
tokenCount := 0
code := "authcode"
azureUser := `{"name":"Azure Test","avatar":{"href":"http://example.com/avatar"}}`
server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser)
defer server.Close()
w := performAuthorizationRequest(ts, "azure", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenWrongToken() {
setupAzureOverrideVerifiers()
ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token")
tokenCount := 0
code := "authcode"
azureUser := `{"name":"Azure Test","avatar":{"href":"http://example.com/avatar"}}`
server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser)
defer server.Close()
w := performAuthorizationRequest(ts, "azure", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenEmailDoesntMatch() {
setupAzureOverrideVerifiers()
ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token")
tokenCount := 0
code := "authcode"
azureUser := `{"name":"Azure Test", "email":"other@example.com", "avatar":{"href":"http://example.com/avatar"}}`
server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser)
defer server.Close()
u := performAuthorization(ts, "azure", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}

View File

@ -0,0 +1,195 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
)
const (
bitbucketUser string = `{"uuid":"bitbucketTestId","display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}`
)
func (ts *ExternalTestSuite) TestSignupExternalBitbucket() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=bitbucket", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Bitbucket.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Bitbucket.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("account email", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("bitbucket", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func BitbucketTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string, emails string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/site/oauth2/access_token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Bitbucket.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"bitbucket_token","expires_in":100000}`)
case "/2.0/user":
*userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, user)
case "/2.0/user/emails":
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, emails)
default:
w.WriteHeader(500)
ts.Fail("unknown bitbucket oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Bitbucket.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalBitbucket_AuthorizationCode() {
ts.Config.DisableSignup = false
tokenCount, userCount := 0, 0
code := "authcode"
emails := `{"values":[{"email":"bitbucket@example.com","is_primary":true,"is_confirmed":true}]}`
server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
defer server.Close()
u := performAuthorization(ts, "bitbucket", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "bitbucketTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}`
emails := `{"values":[{"email":"bitbucket@example.com","is_primary":true,"is_confirmed":true}]}`
server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
defer server.Close()
u := performAuthorization(ts, "bitbucket", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "bitbucket@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupErrorWhenNoEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
emails := `{"values":[{}]}`
server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
defer server.Close()
u := performAuthorization(ts, "bitbucket", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "bitbucket@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("bitbucketTestId", "bitbucket@example.com", "Bitbucket Test", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `{"values":[{"email":"bitbucket@example.com","is_primary":true,"is_confirmed":true}]}`
server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
defer server.Close()
u := performAuthorization(ts, "bitbucket", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "bitbucketTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupSuccessWithSecondaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("bitbucketTestId", "secondary@example.com", "Bitbucket Test", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `{"values":[{"email":"primary@example.com","is_primary":true,"is_confirmed":true},{"email":"secondary@example.com","is_primary":false,"is_confirmed":true}]}`
server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
defer server.Close()
u := performAuthorization(ts, "bitbucket", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "Bitbucket Test", "bitbucketTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketSuccessWhenMatchingToken() {
// name and avatar should be populated from Bitbucket API
ts.createUser("bitbucketTestId", "bitbucket@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `{"values":[{"email":"bitbucket@example.com","is_primary":true,"is_confirmed":true}]}`
server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
defer server.Close()
u := performAuthorization(ts, "bitbucket", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "bitbucketTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}`
emails := `{"values":[{"email":"bitbucket@example.com","is_primary":true,"is_confirmed":true}]}`
server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
defer server.Close()
w := performAuthorizationRequest(ts, "bitbucket", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenWrongToken() {
ts.createUser("bitbucketTestId", "bitbucket@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}`
emails := `{"values":[{"email":"bitbucket@example.com","is_primary":true,"is_confirmed":true}]}`
server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
defer server.Close()
w := performAuthorizationRequest(ts, "bitbucket", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenEmailDoesntMatch() {
ts.createUser("bitbucketTestId", "bitbucket@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `{"values":[{"email":"other@example.com","is_primary":true,"is_confirmed":true}]}`
server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails)
defer server.Close()
u := performAuthorization(ts, "bitbucket", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}

View File

@ -0,0 +1,167 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
)
const (
discordUser string = `{"id":"discordTestId","avatar":"abc","email":"discord@example.com","username":"Discord Test","verified":true,"discriminator":"0001"}}`
discordUserWrongEmail string = `{"id":"discordTestId","avatar":"abc","email":"other@example.com","username":"Discord Test","verified":true}}`
discordUserNoEmail string = `{"id":"discordTestId","avatar":"abc","username":"Discord Test","verified":true}}`
)
func (ts *ExternalTestSuite) TestSignupExternalDiscord() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=discord", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Discord.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Discord.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("email identify", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("discord", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func DiscordTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/oauth2/token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Discord.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"discord_token","expires_in":100000}`)
case "/api/users/@me":
*userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, user)
default:
w.WriteHeader(500)
ts.Fail("unknown discord oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Discord.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalDiscord_AuthorizationCode() {
ts.Config.DisableSignup = false
tokenCount, userCount := 0, 0
code := "authcode"
server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
defer server.Close()
u := performAuthorization(ts, "discord", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "discordTestId", "https://cdn.discordapp.com/avatars/discordTestId/abc.png")
}
func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
defer server.Close()
u := performAuthorization(ts, "discord", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "discord@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUserNoEmail)
defer server.Close()
u := performAuthorization(ts, "discord", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "discord@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("discordTestId", "discord@example.com", "Discord Test", "https://cdn.discordapp.com/avatars/discordTestId/abc.png", "")
tokenCount, userCount := 0, 0
code := "authcode"
server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
defer server.Close()
u := performAuthorization(ts, "discord", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "discordTestId", "https://cdn.discordapp.com/avatars/discordTestId/abc.png")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordSuccessWhenMatchingToken() {
// name and avatar should be populated from Discord API
ts.createUser("discordTestId", "discord@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
defer server.Close()
u := performAuthorization(ts, "discord", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "discordTestId", "https://cdn.discordapp.com/avatars/discordTestId/abc.png")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
defer server.Close()
w := performAuthorizationRequest(ts, "discord", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenWrongToken() {
ts.createUser("discordTestId", "discord@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser)
defer server.Close()
w := performAuthorizationRequest(ts, "discord", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenEmailDoesntMatch() {
ts.createUser("discordTestId", "discord@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUserWrongEmail)
defer server.Close()
u := performAuthorization(ts, "discord", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}

View File

@ -0,0 +1,167 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
)
const (
facebookUser string = `{"id":"facebookTestId","name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}`
facebookUserWrongEmail string = `{"id":"facebookTestId","name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"other@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}`
facebookUserNoEmail string = `{"id":"facebookTestId","name":"Facebook Test","first_name":"Facebook","last_name":"Test","picture":{"data":{"url":"http://example.com/avatar"}}}}`
)
func (ts *ExternalTestSuite) TestSignupExternalFacebook() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=facebook", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Facebook.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Facebook.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("email", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("facebook", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func FacebookTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/oauth/access_token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Facebook.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"facebook_token","expires_in":100000}`)
case "/me":
*userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, user)
default:
w.WriteHeader(500)
ts.Fail("unknown facebook oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Facebook.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalFacebook_AuthorizationCode() {
ts.Config.DisableSignup = false
tokenCount, userCount := 0, 0
code := "authcode"
server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
defer server.Close()
u := performAuthorization(ts, "facebook", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "facebookTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
defer server.Close()
u := performAuthorization(ts, "facebook", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "facebook@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUserNoEmail)
defer server.Close()
u := performAuthorization(ts, "facebook", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "facebook@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("facebookTestId", "facebook@example.com", "Facebook Test", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
defer server.Close()
u := performAuthorization(ts, "facebook", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "facebookTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookSuccessWhenMatchingToken() {
// name and avatar should be populated from Facebook API
ts.createUser("facebookTestId", "facebook@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
defer server.Close()
u := performAuthorization(ts, "facebook", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "facebookTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
defer server.Close()
w := performAuthorizationRequest(ts, "facebook", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenWrongToken() {
ts.createUser("facebookTestId", "facebook@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser)
defer server.Close()
w := performAuthorizationRequest(ts, "facebook", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenEmailDoesntMatch() {
ts.createUser("facebookTestId", "facebook@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUserWrongEmail)
defer server.Close()
u := performAuthorization(ts, "facebook", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}

View File

@ -0,0 +1,264 @@
package api
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"time"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/models"
)
func (ts *ExternalTestSuite) TestSignupExternalFigma() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=figma", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Figma.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Figma.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("files:read", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("figma", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func FigmaTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, email string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/oauth/token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Figma.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"figma_token","expires_in":100000,"refresh_token":"figma_token"}`)
case "/v1/me":
*userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, `{"id":"figma-test-id","email":"%s","handle":"Figma Test","img_url":"http://example.com/avatar"}`, email)
default:
w.WriteHeader(500)
ts.Fail("unknown figma oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Figma.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalFigma_AuthorizationCode() {
tokenCount, userCount := 0, 0
code := "authcode"
email := "figma@example.com"
server := FigmaTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "figma", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "figma@example.com", "Figma Test", "figma-test-id", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalFigma_PKCE() {
tokenCount, userCount := 0, 0
code := "authcode"
// for the plain challenge method, the code verifier == code challenge
// code challenge has to be between 43 - 128 chars for the plain challenge method
codeVerifier := "testtesttesttesttesttesttesttesttesttesttesttesttesttest"
email := "figma@example.com"
server := FigmaTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
cases := []struct {
desc string
codeChallengeMethod string
}{
{
desc: "SHA256",
codeChallengeMethod: "s256",
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
var codeChallenge string
if c.codeChallengeMethod == "s256" {
hashedCodeVerifier := sha256.Sum256([]byte(codeVerifier))
codeChallenge = base64.RawURLEncoding.EncodeToString(hashedCodeVerifier[:])
} else {
codeChallenge = codeVerifier
}
// Check for valid auth code returned
u := performPKCEAuthorization(ts, "figma", code, codeChallenge, c.codeChallengeMethod)
m, err := url.ParseQuery(u.RawQuery)
authCode := m["code"][0]
require.NoError(ts.T(), err)
require.NotEmpty(ts.T(), authCode)
// Check for valid provider access token, mock does not return refresh token
user, err := models.FindUserByEmailAndAudience(ts.API.db, "figma@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
require.NotEmpty(ts.T(), user)
flowState, err := models.FindFlowStateByAuthCode(ts.API.db, authCode)
require.NoError(ts.T(), err)
require.Equal(ts.T(), "figma_token", flowState.ProviderAccessToken)
// Exchange Auth Code for token
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"code_verifier": codeVerifier,
"auth_code": authCode,
}))
req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=pkce", &buffer)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
// Validate that access token and provider tokens are present
data := AccessTokenResponse{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
require.NotEmpty(ts.T(), data.Token)
require.NotEmpty(ts.T(), data.RefreshToken)
require.NotEmpty(ts.T(), data.ProviderAccessToken)
require.Equal(ts.T(), data.User.ID, user.ID)
})
}
}
func (ts *ExternalTestSuite) TestSignupExternalFigmaDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
email := "figma@example.com"
server := FigmaTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "figma", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "figma@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalFigmaDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
email := ""
server := FigmaTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "figma", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "figma@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalFigmaDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("figma-test-id", "figma@example.com", "Figma Test", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
email := "figma@example.com"
server := FigmaTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "figma", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "figma@example.com", "Figma Test", "figma-test-id", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalFigmaSuccessWhenMatchingToken() {
// name and avatar should be populated from Figma API
ts.createUser("figma-test-id", "figma@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
email := "figma@example.com"
server := FigmaTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "figma", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "figma@example.com", "Figma Test", "figma-test-id", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalFigmaErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
email := "figma@example.com"
server := FigmaTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
w := performAuthorizationRequest(ts, "figma", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalFigmaErrorWhenWrongToken() {
ts.createUser("figma-test-id", "figma@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
email := "figma@example.com"
server := FigmaTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
w := performAuthorizationRequest(ts, "figma", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalFigmaErrorWhenEmailDoesntMatch() {
ts.createUser("figma-test-id", "figma@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
email := "other@example.com"
server := FigmaTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "figma", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}
func (ts *ExternalTestSuite) TestSignupExternalFigmaErrorWhenUserBanned() {
tokenCount, userCount := 0, 0
code := "authcode"
email := "figma@example.com"
server := FigmaTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "figma", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "figma@example.com", "Figma Test", "figma-test-id", "http://example.com/avatar")
user, err := models.FindUserByEmailAndAudience(ts.API.db, "figma@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
t := time.Now().Add(24 * time.Hour)
user.BannedUntil = &t
require.NoError(ts.T(), ts.API.db.UpdateOnly(user, "banned_until"))
u = performAuthorization(ts, "figma", code, "")
assertAuthorizationFailure(ts, u, "User is banned", "access_denied", "")
}

View File

@ -0,0 +1,264 @@
package api
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"time"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/models"
)
func (ts *ExternalTestSuite) TestSignupExternalFly() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=fly", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Fly.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Fly.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("read", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("fly", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func FlyTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, email string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/oauth/token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Fly.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"fly_token","expires_in":100000,"refresh_token":"fly_refresh_token"}`)
case "/oauth/token/info":
*userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, `{"resource_owner_id":"test_resource_owner_id","scope":["read"],"expires_in":1111,"application":{"uid":"test_app_uid"},"created_at":1696003692,"user_id":"test_user_id","user_name":"test_user","email":"%s","organizations":[{"id":"test_org_id","role":"test"}]}`, email)
default:
w.WriteHeader(500)
ts.Fail("unknown fly oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Fly.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalFly_AuthorizationCode() {
tokenCount, userCount := 0, 0
code := "authcode"
email := "fly@example.com"
server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "fly", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "fly@example.com", "test_user", "test_user_id", "")
}
func (ts *ExternalTestSuite) TestSignupExternalFly_PKCE() {
tokenCount, userCount := 0, 0
code := "authcode"
// for the plain challenge method, the code verifier == code challenge
// code challenge has to be between 43 - 128 chars for the plain challenge method
codeVerifier := "testtesttesttesttesttesttesttesttesttesttesttesttesttest"
email := "fly@example.com"
server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
cases := []struct {
desc string
codeChallengeMethod string
}{
{
desc: "SHA256",
codeChallengeMethod: "s256",
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
var codeChallenge string
if c.codeChallengeMethod == "s256" {
hashedCodeVerifier := sha256.Sum256([]byte(codeVerifier))
codeChallenge = base64.RawURLEncoding.EncodeToString(hashedCodeVerifier[:])
} else {
codeChallenge = codeVerifier
}
// Check for valid auth code returned
u := performPKCEAuthorization(ts, "fly", code, codeChallenge, c.codeChallengeMethod)
m, err := url.ParseQuery(u.RawQuery)
authCode := m["code"][0]
require.NoError(ts.T(), err)
require.NotEmpty(ts.T(), authCode)
// Check for valid provider access token, mock does not return refresh token
user, err := models.FindUserByEmailAndAudience(ts.API.db, "fly@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
require.NotEmpty(ts.T(), user)
flowState, err := models.FindFlowStateByAuthCode(ts.API.db, authCode)
require.NoError(ts.T(), err)
require.Equal(ts.T(), "fly_token", flowState.ProviderAccessToken)
// Exchange Auth Code for token
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"code_verifier": codeVerifier,
"auth_code": authCode,
}))
req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=pkce", &buffer)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
// Validate that access token and provider tokens are present
data := AccessTokenResponse{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
require.NotEmpty(ts.T(), data.Token)
require.NotEmpty(ts.T(), data.RefreshToken)
require.NotEmpty(ts.T(), data.ProviderAccessToken)
require.Equal(ts.T(), data.User.ID, user.ID)
})
}
}
func (ts *ExternalTestSuite) TestSignupExternalFlyDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
email := "fly@example.com"
server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "fly", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", email)
}
func (ts *ExternalTestSuite) TestSignupExternalFlyDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
email := ""
server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "fly", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "fly@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalFlyDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("test_user_id", "fly@example.com", "test_user", "", "")
tokenCount, userCount := 0, 0
code := "authcode"
email := "fly@example.com"
server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "fly", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "fly@example.com", "test_user", "test_user_id", "")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalFlySuccessWhenMatchingToken() {
// name and avatar should be populated from fly API
ts.createUser("test_user_id", "fly@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
email := "fly@example.com"
server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "fly", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "fly@example.com", "test_user", "test_user_id", "")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalFlyErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
email := "fly@example.com"
server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
w := performAuthorizationRequest(ts, "fly", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalFlyErrorWhenWrongToken() {
ts.createUser("test_user_id", "fly@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
email := "fly@example.com"
server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
w := performAuthorizationRequest(ts, "fly", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalFlyErrorWhenEmailDoesntMatch() {
ts.createUser("test_user_id", "fly@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
email := "other@example.com"
server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "fly", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}
func (ts *ExternalTestSuite) TestSignupExternalFlyErrorWhenUserBanned() {
tokenCount, userCount := 0, 0
code := "authcode"
email := "fly@example.com"
server := FlyTestSignupSetup(ts, &tokenCount, &userCount, code, email)
defer server.Close()
u := performAuthorization(ts, "fly", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "fly@example.com", "test_user", "test_user_id", "")
user, err := models.FindUserByEmailAndAudience(ts.API.db, "fly@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
t := time.Now().Add(24 * time.Hour)
user.BannedUntil = &t
require.NoError(ts.T(), ts.API.db.UpdateOnly(user, "banned_until"))
u = performAuthorization(ts, "fly", code, "")
assertAuthorizationFailure(ts, u, "User is banned", "access_denied", "")
}

View File

@ -0,0 +1,300 @@
package api
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"time"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/models"
)
func (ts *ExternalTestSuite) TestSignupExternalGithub() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=github", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Github.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Github.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("user:email", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("github", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func GitHubTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, emails string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/login/oauth/access_token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Github.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"github_token","expires_in":100000}`)
case "/api/v3/user":
*userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"id":123, "name":"GitHub Test","avatar_url":"http://example.com/avatar"}`)
case "/api/v3/user/emails":
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, emails)
default:
w.WriteHeader(500)
ts.Fail("unknown github oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Github.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalGitHub_AuthorizationCode() {
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"github@example.com", "primary": true, "verified": true}]`
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "github", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "123", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalGitHub_PKCE() {
tokenCount, userCount := 0, 0
code := "authcode"
// for the plain challenge method, the code verifier == code challenge
// code challenge has to be between 43 - 128 chars for the plain challenge method
codeVerifier := "testtesttesttesttesttesttesttesttesttesttesttesttesttest"
emails := `[{"email":"github@example.com", "primary": true, "verified": true}]`
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
cases := []struct {
desc string
codeChallengeMethod string
}{
{
desc: "SHA256",
codeChallengeMethod: "s256",
},
{
desc: "Plain",
codeChallengeMethod: "plain",
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
var codeChallenge string
if c.codeChallengeMethod == "s256" {
hashedCodeVerifier := sha256.Sum256([]byte(codeVerifier))
codeChallenge = base64.RawURLEncoding.EncodeToString(hashedCodeVerifier[:])
} else {
codeChallenge = codeVerifier
}
// Check for valid auth code returned
u := performPKCEAuthorization(ts, "github", code, codeChallenge, c.codeChallengeMethod)
m, err := url.ParseQuery(u.RawQuery)
authCode := m["code"][0]
require.NoError(ts.T(), err)
require.NotEmpty(ts.T(), authCode)
// Check for valid provider access token, mock does not return refresh token
user, err := models.FindUserByEmailAndAudience(ts.API.db, "github@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
require.NotEmpty(ts.T(), user)
flowState, err := models.FindFlowStateByAuthCode(ts.API.db, authCode)
require.NoError(ts.T(), err)
require.Equal(ts.T(), "github_token", flowState.ProviderAccessToken)
// Exchange Auth Code for token
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"code_verifier": codeVerifier,
"auth_code": authCode,
}))
req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=pkce", &buffer)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
// Validate that access token and provider tokens are present
data := AccessTokenResponse{}
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
require.NotEmpty(ts.T(), data.Token)
require.NotEmpty(ts.T(), data.RefreshToken)
require.NotEmpty(ts.T(), data.ProviderAccessToken)
require.Equal(ts.T(), data.User.ID, user.ID)
})
}
}
func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"github@example.com", "primary": true, "verified": true}]`
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "github", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "github@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"primary": true, "verified": true}]`
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "github", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "github@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("123", "github@example.com", "GitHub Test", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"github@example.com", "primary": true, "verified": true}]`
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "github", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "123", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupSuccessWithNonPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("123", "secondary@example.com", "GitHub Test", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"primary@example.com", "primary": true, "verified": true},{"email":"secondary@example.com", "primary": false, "verified": true}]`
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "github", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "GitHub Test", "123", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubSuccessWhenMatchingToken() {
// name and avatar should be populated from GitHub API
ts.createUser("123", "github@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"github@example.com", "primary": true, "verified": true}]`
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "github", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "123", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"github@example.com", "primary": true, "verified": true}]`
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
w := performAuthorizationRequest(ts, "github", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenWrongToken() {
ts.createUser("123", "github@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"github@example.com", "primary": true, "verified": true}]`
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
w := performAuthorizationRequest(ts, "github", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenEmailDoesntMatch() {
ts.createUser("123", "github@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"other@example.com", "primary": true, "verified": true}]`
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "github", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}
func (ts *ExternalTestSuite) TestSignupExternalGitHubErrorWhenVerifiedFalse() {
ts.Config.Mailer.AllowUnverifiedEmailSignIns = false
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"github@example.com", "primary": true, "verified": false}]`
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "github", code, "")
assertAuthorizationFailure(ts, u, "Unverified email with github. A confirmation email has been sent to your github email", "access_denied", "")
}
func (ts *ExternalTestSuite) TestSignupExternalGitHubErrorWhenUserBanned() {
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"github@example.com", "primary": true, "verified": true}]`
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "github", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "123", "http://example.com/avatar")
user, err := models.FindUserByEmailAndAudience(ts.API.db, "github@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
t := time.Now().Add(24 * time.Hour)
user.BannedUntil = &t
require.NoError(ts.T(), ts.API.db.UpdateOnly(user, "banned_until"))
u = performAuthorization(ts, "github", code, "")
assertAuthorizationFailure(ts, u, "User is banned", "access_denied", "")
}

View File

@ -0,0 +1,199 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
)
const (
gitlabUser string = `{"id":123,"email":"gitlab@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}`
gitlabUserWrongEmail string = `{"id":123,"email":"other@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}`
gitlabUserNoEmail string = `{"id":123,"name":"Gitlab Test","avatar_url":"http://example.com/avatar"}`
)
func (ts *ExternalTestSuite) TestSignupExternalGitlab() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=gitlab", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Gitlab.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Gitlab.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("read_user", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("gitlab", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func GitlabTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string, emails string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/oauth/token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Gitlab.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"gitlab_token","expires_in":100000}`)
case "/api/v4/user":
*userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, user)
case "/api/v4/user/emails":
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, emails)
default:
w.WriteHeader(500)
ts.Fail("unknown gitlab oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Gitlab.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalGitlab_AuthorizationCode() {
// additional emails from GitLab don't return confirm status
ts.Config.Mailer.Autoconfirm = true
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"id":1,"email":"gitlab@example.com"}]`
server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
defer server.Close()
u := performAuthorization(ts, "gitlab", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "123", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"id":1,"email":"gitlab@example.com"}]`
server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
defer server.Close()
u := performAuthorization(ts, "gitlab", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "gitlab@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[]`
server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUserNoEmail, emails)
defer server.Close()
u := performAuthorization(ts, "gitlab", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "gitlab@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("123", "gitlab@example.com", "GitLab Test", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
emails := "[]"
server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
defer server.Close()
u := performAuthorization(ts, "gitlab", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "123", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupSuccessWithSecondaryEmail() {
// additional emails from GitLab don't return confirm status
ts.Config.Mailer.Autoconfirm = true
ts.Config.DisableSignup = true
ts.createUser("123", "secondary@example.com", "GitLab Test", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"id":1,"email":"secondary@example.com"}]`
server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
defer server.Close()
u := performAuthorization(ts, "gitlab", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "GitLab Test", "123", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabSuccessWhenMatchingToken() {
// name and avatar should be populated from GitLab API
ts.createUser("123", "gitlab@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
emails := "[]"
server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
defer server.Close()
u := performAuthorization(ts, "gitlab", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "123", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
emails := "[]"
server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
defer server.Close()
w := performAuthorizationRequest(ts, "gitlab", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenWrongToken() {
ts.createUser("123", "gitlab@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
emails := "[]"
server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails)
defer server.Close()
w := performAuthorizationRequest(ts, "gitlab", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenEmailDoesntMatch() {
ts.createUser("123", "gitlab@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
emails := "[]"
server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUserWrongEmail, emails)
defer server.Close()
u := performAuthorization(ts, "gitlab", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}

View File

@ -0,0 +1,181 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/api/provider"
)
const (
googleUser string = `{"id":"googleTestId","name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}`
googleUserWrongEmail string = `{"id":"googleTestId","name":"Google Test","picture":"http://example.com/avatar","email":"other@example.com","verified_email":true}}`
googleUserNoEmail string = `{"id":"googleTestId","name":"Google Test","picture":"http://example.com/avatar","verified_email":false}}`
)
func (ts *ExternalTestSuite) TestSignupExternalGoogle() {
provider.ResetGoogleProvider()
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=google", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Google.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Google.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("email profile", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("google", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func GoogleTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string) *httptest.Server {
provider.ResetGoogleProvider()
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/.well-known/openid-configuration":
w.Header().Add("Content-Type", "application/json")
require.NoError(ts.T(), json.NewEncoder(w).Encode(map[string]any{
"issuer": server.URL,
"token_endpoint": server.URL + "/o/oauth2/token",
}))
case "/o/oauth2/token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Google.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"google_token","expires_in":100000}`)
case "/userinfo/v2/me":
*userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, user)
default:
w.WriteHeader(500)
ts.Fail("unknown google oauth call %s", r.URL.Path)
}
}))
provider.OverrideGoogleProvider(server.URL, server.URL+"/userinfo/v2/me")
return server
}
func (ts *ExternalTestSuite) TestSignupExternalGoogle_AuthorizationCode() {
ts.Config.DisableSignup = false
tokenCount, userCount := 0, 0
code := "authcode"
server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
defer server.Close()
u := performAuthorization(ts, "google", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "googleTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
defer server.Close()
u := performAuthorization(ts, "google", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "google@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUserNoEmail)
defer server.Close()
u := performAuthorization(ts, "google", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "google@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("googleTestId", "google@example.com", "Google Test", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
defer server.Close()
u := performAuthorization(ts, "google", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "googleTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleSuccessWhenMatchingToken() {
// name and avatar should be populated from Google API
ts.createUser("googleTestId", "google@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
defer server.Close()
u := performAuthorization(ts, "google", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "googleTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
defer server.Close()
w := performAuthorizationRequest(ts, "google", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenWrongToken() {
ts.createUser("googleTestId", "google@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser)
defer server.Close()
w := performAuthorizationRequest(ts, "google", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenEmailDoesntMatch() {
ts.createUser("googleTestId", "google@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUserWrongEmail)
defer server.Close()
u := performAuthorization(ts, "google", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}

View File

@ -0,0 +1,238 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"time"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/models"
)
func (ts *ExternalTestSuite) TestSignupExternalKakao() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=kakao", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Kakao.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Kakao.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("kakao", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func KakaoTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, emails string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/oauth/token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Kakao.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"kakao_token","expires_in":100000}`)
case "/v2/user/me":
*userCount++
var emailList []provider.Email
if err := json.Unmarshal([]byte(emails), &emailList); err != nil {
ts.Fail("Invalid email json %s", emails)
}
var email *provider.Email
for i, e := range emailList {
if len(e.Email) > 0 {
email = &emailList[i]
break
}
}
w.Header().Add("Content-Type", "application/json")
if email != nil {
fmt.Fprintf(w, `
{
"id":123,
"kakao_account": {
"profile": {
"nickname":"Kakao Test",
"profile_image_url":"http://example.com/avatar"
},
"email": "%v",
"is_email_valid": %v,
"is_email_verified": %v
}
}`, email.Email, email.Verified, email.Verified)
} else {
fmt.Fprint(w, `
{
"id":123,
"kakao_account": {
"profile": {
"nickname":"Kakao Test",
"profile_image_url":"http://example.com/avatar"
}
}
}`)
}
default:
w.WriteHeader(500)
ts.Fail("unknown kakao oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Kakao.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalKakao_AuthorizationCode() {
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]`
server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "kakao", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "kakao@example.com", "Kakao Test", "123", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalKakaoDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]`
server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "kakao", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "kakao@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalKakaoDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"primary": true, "verified": true}]`
server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "kakao", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "kakao@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalKakaoDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("123", "kakao@example.com", "Kakao Test", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]`
server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "kakao", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "kakao@example.com", "Kakao Test", "123", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalKakaoSuccessWhenMatchingToken() {
// name and avatar should be populated from Kakao API
ts.createUser("123", "kakao@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]`
server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "kakao", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "kakao@example.com", "Kakao Test", "123", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalKakaoErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]`
server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
w := performAuthorizationRequest(ts, "kakao", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalKakaoErrorWhenWrongToken() {
ts.createUser("123", "kakao@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]`
server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
w := performAuthorizationRequest(ts, "kakao", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalKakaoErrorWhenEmailDoesntMatch() {
ts.createUser("123", "kakao@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"other@example.com", "primary": true, "verified": true}]`
server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "kakao", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}
func (ts *ExternalTestSuite) TestSignupExternalKakaoErrorWhenVerifiedFalse() {
ts.Config.Mailer.AllowUnverifiedEmailSignIns = false
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"kakao@example.com", "primary": true, "verified": false}]`
server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "kakao", code, "")
assertAuthorizationFailure(ts, u, "Unverified email with kakao. A confirmation email has been sent to your kakao email", "access_denied", "")
}
func (ts *ExternalTestSuite) TestSignupExternalKakaoErrorWhenUserBanned() {
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]`
server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "kakao", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "kakao@example.com", "Kakao Test", "123", "http://example.com/avatar")
user, err := models.FindUserByEmailAndAudience(ts.API.db, "kakao@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
t := time.Now().Add(24 * time.Hour)
user.BannedUntil = &t
require.NoError(ts.T(), ts.API.db.UpdateOnly(user, "banned_until"))
u = performAuthorization(ts, "kakao", code, "")
assertAuthorizationFailure(ts, u, "User is banned", "access_denied", "")
}

View File

@ -0,0 +1,182 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
)
const (
keycloakUser string = `{"sub": "keycloaktestid", "name": "Keycloak Test", "email": "keycloak@example.com", "preferred_username": "keycloak", "email_verified": true}`
keycloakUserNoEmail string = `{"sub": "keycloaktestid", "name": "Keycloak Test", "preferred_username": "keycloak", "email_verified": false}`
)
func (ts *ExternalTestSuite) TestSignupExternalKeycloak() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=keycloak", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Keycloak.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Keycloak.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("profile email", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("keycloak", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func KeycloakTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/protocol/openid-connect/token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Keycloak.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"keycloak_token","expires_in":100000}`)
case "/protocol/openid-connect/userinfo":
*userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, user)
default:
w.WriteHeader(500)
ts.Fail("unknown keycloak oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Keycloak.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalKeycloakWithoutURLSetup() {
ts.createUser("keycloaktestid", "keycloak@example.com", "Keycloak Test", "", "")
tokenCount, userCount := 0, 0
code := "authcode"
server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser)
ts.Config.External.Keycloak.URL = ""
defer server.Close()
w := performAuthorizationRequest(ts, "keycloak", code)
ts.Equal(w.Code, http.StatusBadRequest)
}
func (ts *ExternalTestSuite) TestSignupExternalKeycloak_AuthorizationCode() {
ts.Config.DisableSignup = false
ts.createUser("keycloaktestid", "keycloak@example.com", "Keycloak Test", "", "")
tokenCount, userCount := 0, 0
code := "authcode"
server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser)
defer server.Close()
u := performAuthorization(ts, "keycloak", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "keycloak@example.com", "Keycloak Test", "keycloaktestid", "")
}
func (ts *ExternalTestSuite) TestSignupExternalKeycloakDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser)
defer server.Close()
u := performAuthorization(ts, "keycloak", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "keycloak@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalKeycloakDisableSignupErrorWhenNoEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUserNoEmail)
defer server.Close()
u := performAuthorization(ts, "keycloak", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "keycloak@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalKeycloakDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("keycloaktestid", "keycloak@example.com", "Keycloak Test", "", "")
tokenCount, userCount := 0, 0
code := "authcode"
server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser)
defer server.Close()
u := performAuthorization(ts, "keycloak", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "keycloak@example.com", "Keycloak Test", "keycloaktestid", "")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalKeycloakSuccessWhenMatchingToken() {
// name and avatar should be populated from Keycloak API
ts.createUser("keycloaktestid", "keycloak@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser)
defer server.Close()
u := performAuthorization(ts, "keycloak", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "keycloak@example.com", "Keycloak Test", "keycloaktestid", "")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalKeycloakErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
keycloakUser := `{"name":"Keycloak Test","avatar":{"href":"http://example.com/avatar"}}`
server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser)
defer server.Close()
w := performAuthorizationRequest(ts, "keycloak", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalKeycloakErrorWhenWrongToken() {
ts.createUser("keycloaktestid", "keycloak@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
keycloakUser := `{"name":"Keycloak Test","avatar":{"href":"http://example.com/avatar"}}`
server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser)
defer server.Close()
w := performAuthorizationRequest(ts, "keycloak", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalKeycloakErrorWhenEmailDoesntMatch() {
ts.createUser("keycloaktestid", "keycloak@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
keycloakUser := `{"name":"Keycloak Test", "email":"other@example.com", "avatar":{"href":"http://example.com/avatar"}}`
server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser)
defer server.Close()
u := performAuthorization(ts, "keycloak", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}

View File

@ -0,0 +1,170 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
)
const (
linkedinUser string = `{"id":"linkedinTestId","firstName":{"localized":{"en_US":"Linkedin"},"preferredLocale":{"country":"US","language":"en"}},"lastName":{"localized":{"en_US":"Test"},"preferredLocale":{"country":"US","language":"en"}},"profilePicture":{"displayImage~":{"elements":[{"identifiers":[{"identifier":"http://example.com/avatar"}]}]}}}`
linkedinUserNoProfilePic string = `{"id":"linkedinTestId","firstName":{"localized":{"en_US":"Linkedin"},"preferredLocale":{"country":"US","language":"en"}},"lastName":{"localized":{"en_US":"Test"},"preferredLocale":{"country":"US","language":"en"}},"profilePicture":{"displayImage~":{"elements":[]}}}`
linkedinEmail string = `{"elements": [{"handle": "","handle~": {"emailAddress": "linkedin@example.com"}}]}`
linkedinWrongEmail string = `{"elements": [{"handle": "","handle~": {"emailAddress": "other@example.com"}}]}`
)
func (ts *ExternalTestSuite) TestSignupExternalLinkedin() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=linkedin", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Linkedin.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Linkedin.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("r_emailaddress r_liteprofile", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("linkedin", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func LinkedinTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string, email string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/oauth/v2/accessToken":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Linkedin.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"linkedin_token","expires_in":100000}`)
case "/v2/me":
*userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, user)
case "/v2/emailAddress":
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, email)
default:
w.WriteHeader(500)
ts.Fail("unknown linkedin oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Linkedin.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalLinkedin_AuthorizationCode() {
ts.Config.DisableSignup = false
tokenCount, userCount := 0, 0
code := "authcode"
server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinEmail)
defer server.Close()
u := performAuthorization(ts, "linkedin", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "linkedin@example.com", "Linkedin Test", "linkedinTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalLinkedinDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinEmail)
defer server.Close()
u := performAuthorization(ts, "linkedin", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "linkedin@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalLinkedinDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("linkedinTestId", "linkedin@example.com", "Linkedin Test", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinEmail)
defer server.Close()
u := performAuthorization(ts, "linkedin", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "linkedin@example.com", "Linkedin Test", "linkedinTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalLinkedinSuccessWhenMatchingToken() {
// name and avatar should be populated from Linkedin API
ts.createUser("linkedinTestId", "linkedin@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinEmail)
defer server.Close()
u := performAuthorization(ts, "linkedin", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "linkedin@example.com", "Linkedin Test", "linkedinTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalLinkedinErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinEmail)
defer server.Close()
w := performAuthorizationRequest(ts, "linkedin", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalLinkedinErrorWhenWrongToken() {
ts.createUser("linkedinTestId", "linkedin@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinEmail)
defer server.Close()
w := performAuthorizationRequest(ts, "linkedin", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalLinkedinErrorWhenEmailDoesntMatch() {
ts.createUser("linkedinTestId", "linkedin@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinWrongEmail)
defer server.Close()
u := performAuthorization(ts, "linkedin", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}
func (ts *ExternalTestSuite) TestSignupExternalLinkedin_MissingProfilePic() {
tokenCount, userCount := 0, 0
code := "authcode"
server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUserNoProfilePic, linkedinEmail)
defer server.Close()
u := performAuthorization(ts, "linkedin", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "linkedin@example.com", "Linkedin Test", "linkedinTestId", "")
}

View File

@ -0,0 +1,170 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
)
const (
notionUser string = `{"bot":{"owner":{"user":{"id":"notionTestId","name":"Notion Test","avatar_url":"http://example.com/avatar","person":{"email":"notion@example.com"},"verified_email":true}}}}`
notionUserWrongEmail string = `{"bot":{"owner":{"user":{"id":"notionTestId","name":"Notion Test","avatar_url":"http://example.com/avatar","person":{"email":"other@example.com"},"verified_email":true}}}}`
notionUserNoEmail string = `{"bot":{"owner":{"user":{"id":"notionTestId","name":"Notion Test","avatar_url":"http://example.com/avatar","verified_email":true}}}}`
)
func (ts *ExternalTestSuite) TestSignupExternalNotion() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=notion", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Notion.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Notion.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("notion", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func NotionTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/oauth/token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Notion.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"notion_token","expires_in":100000}`)
case "/v1/users/me":
*userCount++
ts.Contains(r.Header, "Authorization")
ts.Contains(r.Header, "Notion-Version")
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, user)
default:
w.WriteHeader(500)
ts.Fail("unknown notion oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Notion.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalNotion_AuthorizationCode() {
ts.Config.DisableSignup = false
tokenCount, userCount := 0, 0
code := "authcode"
server := NotionTestSignupSetup(ts, &tokenCount, &userCount, code, notionUser)
defer server.Close()
u := performAuthorization(ts, "notion", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "notion@example.com", "Notion Test", "notionTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalNotionDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := NotionTestSignupSetup(ts, &tokenCount, &userCount, code, notionUser)
defer server.Close()
u := performAuthorization(ts, "notion", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "notion@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalNotionDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := NotionTestSignupSetup(ts, &tokenCount, &userCount, code, notionUserNoEmail)
defer server.Close()
u := performAuthorization(ts, "notion", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "notion@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalNotionDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("notionTestId", "notion@example.com", "Notion Test", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
server := NotionTestSignupSetup(ts, &tokenCount, &userCount, code, notionUser)
defer server.Close()
u := performAuthorization(ts, "notion", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "notion@example.com", "Notion Test", "notionTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalNotionSuccessWhenMatchingToken() {
// name and avatar should be populated from Notion API
ts.createUser("notionTestId", "notion@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := NotionTestSignupSetup(ts, &tokenCount, &userCount, code, notionUser)
defer server.Close()
u := performAuthorization(ts, "notion", code, "invite_token")
fmt.Printf("%+v\n", u)
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "notion@example.com", "Notion Test", "notionTestId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalNotionErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
server := NotionTestSignupSetup(ts, &tokenCount, &userCount, code, notionUser)
defer server.Close()
w := performAuthorizationRequest(ts, "notion", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalNotionErrorWhenWrongToken() {
ts.createUser("notionTestId", "notion@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := NotionTestSignupSetup(ts, &tokenCount, &userCount, code, notionUser)
defer server.Close()
w := performAuthorizationRequest(ts, "notion", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalNotionErrorWhenEmailDoesntMatch() {
ts.createUser("notionTestId", "notion@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := NotionTestSignupSetup(ts, &tokenCount, &userCount, code, notionUserWrongEmail)
defer server.Close()
u := performAuthorization(ts, "notion", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}

View File

@ -0,0 +1,155 @@
package api
import (
"context"
"fmt"
"net/http"
"net/url"
"github.com/mrjones/oauth"
"github.com/sirupsen/logrus"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/observability"
"github.com/supabase/auth/internal/utilities"
)
// OAuthProviderData contains the userData and token returned by the oauth provider
type OAuthProviderData struct {
userData *provider.UserProvidedData
token string
refreshToken string
code string
}
// loadFlowState parses the `state` query parameter as a JWS payload,
// extracting the provider requested
func (a *API) loadFlowState(w http.ResponseWriter, r *http.Request) (context.Context, error) {
ctx := r.Context()
oauthToken := r.URL.Query().Get("oauth_token")
if oauthToken != "" {
ctx = withRequestToken(ctx, oauthToken)
}
oauthVerifier := r.URL.Query().Get("oauth_verifier")
if oauthVerifier != "" {
ctx = withOAuthVerifier(ctx, oauthVerifier)
}
var err error
ctx, err = a.loadExternalState(ctx, r)
if err != nil {
u, uerr := url.ParseRequestURI(a.config.SiteURL)
if uerr != nil {
return ctx, internalServerError("site url is improperly formatted").WithInternalError(uerr)
}
q := getErrorQueryString(err, utilities.GetRequestID(ctx), observability.GetLogEntry(r).Entry, u.Query())
u.RawQuery = q.Encode()
http.Redirect(w, r, u.String(), http.StatusSeeOther)
}
return ctx, err
}
func (a *API) oAuthCallback(ctx context.Context, r *http.Request, providerType string) (*OAuthProviderData, error) {
var rq url.Values
if err := r.ParseForm(); r.Method == http.MethodPost && err == nil {
rq = r.Form
} else {
rq = r.URL.Query()
}
extError := rq.Get("error")
if extError != "" {
return nil, oauthError(extError, rq.Get("error_description"))
}
oauthCode := rq.Get("code")
if oauthCode == "" {
return nil, badRequestError(ErrorCodeBadOAuthCallback, "OAuth callback with missing authorization code missing")
}
oAuthProvider, err := a.OAuthProvider(ctx, providerType)
if err != nil {
return nil, badRequestError(ErrorCodeOAuthProviderNotSupported, "Unsupported provider: %+v", err).WithInternalError(err)
}
log := observability.GetLogEntry(r).Entry
log.WithFields(logrus.Fields{
"provider": providerType,
"code": oauthCode,
}).Debug("Exchanging oauth code")
token, err := oAuthProvider.GetOAuthToken(oauthCode)
if err != nil {
return nil, internalServerError("Unable to exchange external code: %s", oauthCode).WithInternalError(err)
}
userData, err := oAuthProvider.GetUserData(ctx, token)
if err != nil {
return nil, internalServerError("Error getting user profile from external provider").WithInternalError(err)
}
switch externalProvider := oAuthProvider.(type) {
case *provider.AppleProvider:
// apple only returns user info the first time
oauthUser := rq.Get("user")
if oauthUser != "" {
err := externalProvider.ParseUser(oauthUser, userData)
if err != nil {
return nil, err
}
}
}
return &OAuthProviderData{
userData: userData,
token: token.AccessToken,
refreshToken: token.RefreshToken,
code: oauthCode,
}, nil
}
func (a *API) oAuth1Callback(ctx context.Context, providerType string) (*OAuthProviderData, error) {
oAuthProvider, err := a.OAuthProvider(ctx, providerType)
if err != nil {
return nil, badRequestError(ErrorCodeOAuthProviderNotSupported, "Unsupported provider: %+v", err).WithInternalError(err)
}
oauthToken := getRequestToken(ctx)
oauthVerifier := getOAuthVerifier(ctx)
var accessToken *oauth.AccessToken
var userData *provider.UserProvidedData
if twitterProvider, ok := oAuthProvider.(*provider.TwitterProvider); ok {
accessToken, err = twitterProvider.Consumer.AuthorizeToken(&oauth.RequestToken{
Token: oauthToken,
}, oauthVerifier)
if err != nil {
return nil, internalServerError("Unable to retrieve access token").WithInternalError(err)
}
userData, err = twitterProvider.FetchUserData(ctx, accessToken)
if err != nil {
return nil, internalServerError("Error getting user email from external provider").WithInternalError(err)
}
}
return &OAuthProviderData{
userData: userData,
token: accessToken.Token,
refreshToken: "",
}, nil
}
// OAuthProvider returns the corresponding oauth provider as an OAuthProvider interface
func (a *API) OAuthProvider(ctx context.Context, name string) (provider.OAuthProvider, error) {
providerCandidate, err := a.Provider(ctx, name, "")
if err != nil {
return nil, err
}
switch p := providerCandidate.(type) {
case provider.OAuthProvider:
return p, nil
default:
return nil, fmt.Errorf("Provider %v cannot be used for OAuth", name)
}
}

View File

@ -0,0 +1,33 @@
package api
import (
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
)
func (ts *ExternalTestSuite) TestSignupExternalSlackOIDC() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=slack_oidc", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Slack.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Slack.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("profile email openid", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("slack_oidc", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}

View File

@ -0,0 +1,254 @@
package api
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/models"
)
type ExternalTestSuite struct {
suite.Suite
API *API
Config *conf.GlobalConfiguration
}
func TestExternal(t *testing.T) {
api, config, err := setupAPIForTest()
require.NoError(t, err)
ts := &ExternalTestSuite{
API: api,
Config: config,
}
defer api.db.Close()
suite.Run(t, ts)
}
func (ts *ExternalTestSuite) SetupTest() {
ts.Config.DisableSignup = false
ts.Config.Mailer.Autoconfirm = false
models.TruncateAll(ts.API.db)
}
func (ts *ExternalTestSuite) createUser(providerId string, email string, name string, avatar string, confirmationToken string) (*models.User, error) {
// Cleanup existing user, if they already exist
if u, _ := models.FindUserByEmailAndAudience(ts.API.db, email, ts.Config.JWT.Aud); u != nil {
require.NoError(ts.T(), ts.API.db.Destroy(u), "Error deleting user")
}
userData := map[string]interface{}{"provider_id": providerId, "full_name": name}
if avatar != "" {
userData["avatar_url"] = avatar
}
u, err := models.NewUser("", email, "test", ts.Config.JWT.Aud, userData)
if confirmationToken != "" {
u.ConfirmationToken = confirmationToken
}
ts.Require().NoError(err, "Error making new user")
ts.Require().NoError(ts.API.db.Create(u), "Error creating user")
if confirmationToken != "" {
ts.Require().NoError(models.CreateOneTimeToken(ts.API.db, u.ID, email, u.ConfirmationToken, models.ConfirmationToken), "Error creating one-time confirmation/invite token")
}
i, err := models.NewIdentity(u, "email", map[string]interface{}{
"sub": u.ID.String(),
"email": email,
})
ts.Require().NoError(err)
ts.Require().NoError(ts.API.db.Create(i), "Error creating identity")
return u, err
}
func performAuthorizationRequest(ts *ExternalTestSuite, provider string, inviteToken string) *httptest.ResponseRecorder {
authorizeURL := "http://localhost/authorize?provider=" + provider
if inviteToken != "" {
authorizeURL = authorizeURL + "&invite_token=" + inviteToken
}
req := httptest.NewRequest(http.MethodGet, authorizeURL, nil)
req.Header.Set("Referer", "https://example.netlify.com/admin")
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
return w
}
func performPKCEAuthorizationRequest(ts *ExternalTestSuite, provider, codeChallenge, codeChallengeMethod string) *httptest.ResponseRecorder {
authorizeURL := "http://localhost/authorize?provider=" + provider
if codeChallenge != "" {
authorizeURL = authorizeURL + "&code_challenge=" + codeChallenge + "&code_challenge_method=" + codeChallengeMethod
}
req := httptest.NewRequest(http.MethodGet, authorizeURL, nil)
req.Header.Set("Referer", "https://example.supabase.com/admin")
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
return w
}
func performPKCEAuthorization(ts *ExternalTestSuite, provider, code, codeChallenge, codeChallengeMethod string) *url.URL {
w := performPKCEAuthorizationRequest(ts, provider, codeChallenge, codeChallengeMethod)
ts.Require().Equal(http.StatusFound, w.Code)
// Get code and state from the redirect
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
state := q.Get("state")
testURL, err := url.Parse("http://localhost/callback")
ts.Require().NoError(err)
v := testURL.Query()
v.Set("code", code)
v.Set("state", state)
testURL.RawQuery = v.Encode()
// Use the code to get a token
req := httptest.NewRequest(http.MethodGet, testURL.String(), nil)
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err = url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
return u
}
func performAuthorization(ts *ExternalTestSuite, provider string, code string, inviteToken string) *url.URL {
w := performAuthorizationRequest(ts, provider, inviteToken)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
state := q.Get("state")
// auth server callback
testURL, err := url.Parse("http://localhost/callback")
ts.Require().NoError(err)
v := testURL.Query()
v.Set("code", code)
v.Set("state", state)
testURL.RawQuery = v.Encode()
req := httptest.NewRequest(http.MethodGet, testURL.String(), nil)
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err = url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
ts.Require().Equal("/admin", u.Path)
return u
}
func assertAuthorizationSuccess(ts *ExternalTestSuite, u *url.URL, tokenCount int, userCount int, email string, name string, providerId string, avatar string) {
// ensure redirect has #access_token=...
v, err := url.ParseQuery(u.RawQuery)
ts.Require().NoError(err)
ts.Require().Empty(v.Get("error_description"))
ts.Require().Empty(v.Get("error"))
v, err = url.ParseQuery(u.Fragment)
ts.Require().NoError(err)
ts.NotEmpty(v.Get("access_token"))
ts.NotEmpty(v.Get("refresh_token"))
ts.NotEmpty(v.Get("expires_in"))
ts.Equal("bearer", v.Get("token_type"))
ts.Equal(1, tokenCount)
if userCount > -1 {
ts.Equal(1, userCount)
}
// ensure user has been created with metadata
user, err := models.FindUserByEmailAndAudience(ts.API.db, email, ts.Config.JWT.Aud)
ts.Require().NoError(err)
ts.Equal(providerId, user.UserMetaData["provider_id"])
ts.Equal(name, user.UserMetaData["full_name"])
if avatar == "" {
ts.Equal(nil, user.UserMetaData["avatar_url"])
} else {
ts.Equal(avatar, user.UserMetaData["avatar_url"])
}
}
func assertAuthorizationFailure(ts *ExternalTestSuite, u *url.URL, errorDescription string, errorType string, email string) {
// ensure new sign ups error
v, err := url.ParseQuery(u.RawQuery)
ts.Require().NoError(err)
ts.Require().Equal(errorDescription, v.Get("error_description"))
ts.Require().Equal(errorType, v.Get("error"))
v, err = url.ParseQuery(u.Fragment)
ts.Require().NoError(err)
ts.Empty(v.Get("access_token"))
ts.Empty(v.Get("refresh_token"))
ts.Empty(v.Get("expires_in"))
ts.Empty(v.Get("token_type"))
// ensure user is nil
user, err := models.FindUserByEmailAndAudience(ts.API.db, email, ts.Config.JWT.Aud)
ts.Require().Error(err, "User not found")
ts.Require().Nil(user)
}
// TestSignupExternalUnsupported tests API /authorize for an unsupported external provider
func (ts *ExternalTestSuite) TestSignupExternalUnsupported() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=external", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Equal(w.Code, http.StatusBadRequest)
}
func (ts *ExternalTestSuite) TestRedirectErrorsShouldPreserveParams() {
// Request with invalid external provider
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=external", nil)
w := httptest.NewRecorder()
cases := []struct {
Desc string
RedirectURL string
QueryParams []string
ErrorMessage string
}{
{
Desc: "Should preserve redirect query params on error",
RedirectURL: "http://example.com/path?paramforpreservation=value2",
QueryParams: []string{"paramforpreservation"},
ErrorMessage: "invalid_request",
},
{
Desc: "Error param should be overwritten",
RedirectURL: "http://example.com/path?error=abc",
QueryParams: []string{"error"},
ErrorMessage: "invalid_request",
},
}
for _, c := range cases {
parsedURL, err := url.Parse(c.RedirectURL)
require.Equal(ts.T(), err, nil)
redirectErrors(ts.API.internalExternalProviderCallback, w, req, parsedURL)
parsedParams, err := url.ParseQuery(parsedURL.RawQuery)
require.Equal(ts.T(), err, nil)
// An error and description should be returned
expectedQueryParams := append(c.QueryParams, "error", "error_description")
for _, expectedQueryParam := range expectedQueryParams {
val, exists := parsedParams[expectedQueryParam]
require.True(ts.T(), exists)
if expectedQueryParam == "error" {
require.Equal(ts.T(), val[0], c.ErrorMessage)
}
}
}
}

View File

@ -0,0 +1,171 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
)
const (
twitchUser string = `{"data":[{"id":"twitchTestId","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}`
twitchUserWrongEmail string = `{"data":[{"id":"twitchTestId","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"other@example.com"}]}`
)
func (ts *ExternalTestSuite) TestSignupExternalTwitch() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=twitch", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Twitch.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Twitch.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("user:read:email", q.Get("scope"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("twitch", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func TwitchTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/oauth2/token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Twitch.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"Twitch_token","expires_in":100000}`)
case "/helix/users":
*userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, user)
default:
w.WriteHeader(500)
ts.Fail("unknown Twitch oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Twitch.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalTwitch_AuthorizationCode() {
ts.Config.DisableSignup = false
tokenCount, userCount := 0, 0
code := "authcode"
server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, twitchUser)
defer server.Close()
u := performAuthorization(ts, "twitch", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch user", "twitchTestId", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8")
}
func (ts *ExternalTestSuite) TestSignupExternalTwitchDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
TwitchUser := `{"data":[{"id":"1","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}`
server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser)
defer server.Close()
u := performAuthorization(ts, "twitch", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "twitch@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalTwitchDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
TwitchUser := `{"data":[{"id":"1","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":""}]}`
server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser)
defer server.Close()
u := performAuthorization(ts, "twitch", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "twitch@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalTwitchDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("twitchTestId", "twitch@example.com", "Twitch user", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8", "")
tokenCount, userCount := 0, 0
code := "authcode"
server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, twitchUser)
defer server.Close()
u := performAuthorization(ts, "twitch", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch user", "twitchTestId", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchSuccessWhenMatchingToken() {
// name and avatar should be populated from Twitch API
ts.createUser("twitchTestId", "twitch@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
TwitchUser := `{"data":[{"id":"twitchTestId","login":"Twitch Test","display_name":"Twitch Test","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}`
server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser)
defer server.Close()
u := performAuthorization(ts, "twitch", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch Test", "twitchTestId", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
TwitchUser := `{"data":[{"id":"1","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}`
server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser)
defer server.Close()
w := performAuthorizationRequest(ts, "twitch", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenWrongToken() {
ts.createUser("twitchTestId", "twitch@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, twitchUser)
defer server.Close()
w := performAuthorizationRequest(ts, "twitch", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenEmailDoesntMatch() {
ts.createUser("twitchTestId", "twitch@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, twitchUserWrongEmail)
defer server.Close()
u := performAuthorization(ts, "twitch", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}

View File

@ -0,0 +1,42 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
)
func (ts *ExternalTestSuite) TestSignupExternalTwitter() {
server := TwitterTestSignupSetup(ts, nil, nil, "", "")
defer server.Close()
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=twitter", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
// Twitter uses OAuth1.0 protocol which only returns an oauth_token on the redirect
q := u.Query()
ts.Equal("twitter_oauth_token", q.Get("oauth_token"))
}
func TwitterTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/oauth/request_token":
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, "oauth_token=twitter_oauth_token&oauth_token_secret=twitter_oauth_token_secret&oauth_callback_confirmed=true")
default:
w.WriteHeader(500)
ts.Fail("unknown google oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Twitter.URL = server.URL
return server
}

View File

@ -0,0 +1,221 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
)
const (
workosUser string = `{"id":"test_prof_workos","first_name":"John","last_name":"Doe","email":"workos@example.com","connection_id":"test_conn_1","organization_id":"test_org_1","connection_type":"test","idp_id":"test_idp_1","object": "profile","raw_attributes": {}}`
workosUserWrongEmail string = `{"id":"test_prof_workos","first_name":"John","last_name":"Doe","email":"other@example.com","connection_id":"test_conn_1","organization_id":"test_org_1","connection_type":"test","idp_id":"test_idp_1","object": "profile","raw_attributes": {}}`
workosUserNoEmail string = `{"id":"test_prof_workos","first_name":"John","last_name":"Doe","connection_id":"test_conn_1","organization_id":"test_org_1","connection_type":"test","idp_id":"test_idp_1","object": "profile","raw_attributes": {}}`
)
func (ts *ExternalTestSuite) TestSignupExternalWorkOSWithConnection() {
connection := "test_connection_id"
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost/authorize?provider=workos&connection=%s", connection), nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.WorkOS.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.WorkOS.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("", q.Get("scope"))
ts.Equal(connection, q.Get("connection"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("workos", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func (ts *ExternalTestSuite) TestSignupExternalWorkOSWithOrganization() {
organization := "test_organization_id"
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost/authorize?provider=workos&organization=%s", organization), nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.WorkOS.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.WorkOS.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("", q.Get("scope"))
ts.Equal(organization, q.Get("organization"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("workos", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func (ts *ExternalTestSuite) TestSignupExternalWorkOSWithProvider() {
provider := "test_provider"
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost/authorize?provider=workos&workos_provider=%s", provider), nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.WorkOS.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.WorkOS.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("", q.Get("scope"))
ts.Equal(provider, q.Get("provider"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("workos", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func WorkosTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/sso/token":
// WorkOS returns the user data along with the token.
*tokenCount++
*userCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.WorkOS.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, `{"access_token":"workos_token","expires_in":100000,"profile":%s}`, user)
default:
fmt.Printf("%s", r.URL.Path)
w.WriteHeader(500)
ts.Fail("unknown workos oauth call %s", r.URL.Path)
}
}))
ts.Config.External.WorkOS.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalWorkosAuthorizationCode() {
ts.Config.DisableSignup = false
tokenCount, userCount := 0, 0
code := "authcode"
server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUser)
defer server.Close()
u := performAuthorization(ts, "workos", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "workos@example.com", "John Doe", "test_prof_workos", "")
}
func (ts *ExternalTestSuite) TestSignupExternalWorkosDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUser)
defer server.Close()
u := performAuthorization(ts, "workos", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "workos@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalWorkosDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUserNoEmail)
defer server.Close()
u := performAuthorization(ts, "workos", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "workos@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalWorkosDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("test_prof_workos", "workos@example.com", "John Doe", "", "")
tokenCount, userCount := 0, 0
code := "authcode"
server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUser)
defer server.Close()
u := performAuthorization(ts, "workos", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "workos@example.com", "John Doe", "test_prof_workos", "")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalWorkosSuccessWhenMatchingToken() {
ts.createUser("test_prof_workos", "workos@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUser)
defer server.Close()
u := performAuthorization(ts, "workos", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "workos@example.com", "John Doe", "test_prof_workos", "")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalWorkosErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUser)
defer server.Close()
w := performAuthorizationRequest(ts, "workos", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalWorkosErrorWhenWrongToken() {
ts.createUser("test_prof_workos", "workos@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUser)
defer server.Close()
w := performAuthorizationRequest(ts, "workos", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalWorkosErrorWhenEmailDoesntMatch() {
ts.createUser("test_prof_workos", "workos@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUserWrongEmail)
defer server.Close()
u := performAuthorization(ts, "workos", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}

View File

@ -0,0 +1,167 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
jwt "github.com/golang-jwt/jwt/v5"
)
const (
zoomUser string = `{"id":"zoomUserId","first_name":"John","last_name": "Doe","email": "zoom@example.com","verified": 1,"pic_url":"http://example.com/avatar"}`
zoomUserWrongEmail string = `{"id":"zoomUserId","first_name":"John","last_name": "Doe","email": "other@example.com","verified": 1,"pic_url":"http://example.com/avatar"}`
zoomUserNoEmail string = `{"id":"zoomUserId","first_name":"John","last_name": "Doe","verified": 1,"pic_url":"http://example.com/avatar"}`
)
func (ts *ExternalTestSuite) TestSignupExternalZoom() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=zoom", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Zoom.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Zoom.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)
ts.Equal("zoom", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
func ZoomTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/oauth/token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Zoom.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"zoom_token","expires_in":100000}`)
case "/v2/users/me":
*userCount++
ts.Contains(r.Header, "Authorization")
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, user)
default:
w.WriteHeader(500)
ts.Fail("unknown zoom oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Zoom.URL = server.URL
return server
}
func (ts *ExternalTestSuite) TestSignupExternalZoomAuthorizationCode() {
ts.Config.DisableSignup = false
tokenCount, userCount := 0, 0
code := "authcode"
server := ZoomTestSignupSetup(ts, &tokenCount, &userCount, code, zoomUser)
defer server.Close()
u := performAuthorization(ts, "zoom", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "zoom@example.com", "John Doe", "zoomUserId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestSignupExternalZoomDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := ZoomTestSignupSetup(ts, &tokenCount, &userCount, code, zoomUser)
defer server.Close()
u := performAuthorization(ts, "zoom", code, "")
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "zoom@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalZoomDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
server := ZoomTestSignupSetup(ts, &tokenCount, &userCount, code, zoomUserNoEmail)
defer server.Close()
u := performAuthorization(ts, "zoom", code, "")
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "zoom@example.com")
}
func (ts *ExternalTestSuite) TestSignupExternalZoomDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true
ts.createUser("zoomUserId", "zoom@example.com", "John Doe", "http://example.com/avatar", "")
tokenCount, userCount := 0, 0
code := "authcode"
server := ZoomTestSignupSetup(ts, &tokenCount, &userCount, code, zoomUser)
defer server.Close()
u := performAuthorization(ts, "zoom", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "zoom@example.com", "John Doe", "zoomUserId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalZoomSuccessWhenMatchingToken() {
ts.createUser("zoomUserId", "zoom@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := ZoomTestSignupSetup(ts, &tokenCount, &userCount, code, zoomUser)
defer server.Close()
u := performAuthorization(ts, "zoom", code, "invite_token")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "zoom@example.com", "John Doe", "zoomUserId", "http://example.com/avatar")
}
func (ts *ExternalTestSuite) TestInviteTokenExternalZoomErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
server := ZoomTestSignupSetup(ts, &tokenCount, &userCount, code, zoomUser)
defer server.Close()
w := performAuthorizationRequest(ts, "zoom", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalZoomErrorWhenWrongToken() {
ts.createUser("zoomUserId", "zoom@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := ZoomTestSignupSetup(ts, &tokenCount, &userCount, code, zoomUser)
defer server.Close()
w := performAuthorizationRequest(ts, "zoom", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}
func (ts *ExternalTestSuite) TestInviteTokenExternalZoomErrorWhenEmailDoesntMatch() {
ts.createUser("zoomUserId", "zoom@example.com", "", "", "invite_token")
tokenCount, userCount := 0, 0
code := "authcode"
server := ZoomTestSignupSetup(ts, &tokenCount, &userCount, code, zoomUserWrongEmail)
defer server.Close()
u := performAuthorization(ts, "zoom", code, "invite_token")
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}

View File

@ -0,0 +1,103 @@
package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/pkg/errors"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/security"
"github.com/supabase/auth/internal/utilities"
)
func sendJSON(w http.ResponseWriter, status int, obj interface{}) error {
w.Header().Set("Content-Type", "application/json")
b, err := json.Marshal(obj)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("Error encoding json response: %v", obj))
}
w.WriteHeader(status)
_, err = w.Write(b)
return err
}
func isAdmin(u *models.User, config *conf.GlobalConfiguration) bool {
return config.JWT.Aud == u.Aud && u.HasRole(config.JWT.AdminGroupName)
}
func (a *API) requestAud(ctx context.Context, r *http.Request) string {
config := a.config
// First check for an audience in the header
if aud := r.Header.Get(audHeaderName); aud != "" {
return aud
}
// Then check the token
claims := getClaims(ctx)
if claims != nil {
aud, _ := claims.GetAudience()
if len(aud) != 0 && aud[0] != "" {
return aud[0]
}
}
// Finally, return the default if none of the above methods are successful
return config.JWT.Aud
}
func isStringInSlice(checkValue string, list []string) bool {
for _, val := range list {
if val == checkValue {
return true
}
}
return false
}
type RequestParams interface {
AdminUserParams |
CreateSSOProviderParams |
EnrollFactorParams |
GenerateLinkParams |
IdTokenGrantParams |
InviteParams |
OtpParams |
PKCEGrantParams |
PasswordGrantParams |
RecoverParams |
RefreshTokenGrantParams |
ResendConfirmationParams |
SignupParams |
SingleSignOnParams |
SmsParams |
UserUpdateParams |
VerifyFactorParams |
VerifyParams |
adminUserUpdateFactorParams |
adminUserDeleteParams |
security.GotrueRequest |
ChallengeFactorParams |
struct {
Email string `json:"email"`
Phone string `json:"phone"`
} |
struct {
Email string `json:"email"`
}
}
// retrieveRequestParams is a generic method that unmarshals the request body into the params struct provided
func retrieveRequestParams[A RequestParams](r *http.Request, params *A) error {
body, err := utilities.GetBodyBytes(r)
if err != nil {
return internalServerError("Could not read body into byte slice").WithInternalError(err)
}
if err := json.Unmarshal(body, params); err != nil {
return badRequestError(ErrorCodeBadJSON, "Could not parse request body as JSON: %v", err)
}
return nil
}

View File

@ -0,0 +1,151 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/conf"
)
func TestIsValidCodeChallenge(t *testing.T) {
cases := []struct {
challenge string
isValid bool
expectedError error
}{
{
challenge: "invalid",
isValid: false,
expectedError: badRequestError(ErrorCodeValidationFailed, "code challenge has to be between %v and %v characters", MinCodeChallengeLength, MaxCodeChallengeLength),
},
{
challenge: "codechallengecontainsinvalidcharacterslike@$^&*",
isValid: false,
expectedError: badRequestError(ErrorCodeValidationFailed, "code challenge can only contain alphanumeric characters, hyphens, periods, underscores and tildes"),
},
{
challenge: "validchallengevalidchallengevalidchallengevalidchallenge",
isValid: true,
expectedError: nil,
},
}
for _, c := range cases {
valid, err := isValidCodeChallenge(c.challenge)
require.Equal(t, c.isValid, valid)
require.Equal(t, c.expectedError, err)
}
}
func TestIsValidPKCEParams(t *testing.T) {
cases := []struct {
challengeMethod string
challenge string
expected error
}{
{
challengeMethod: "",
challenge: "",
expected: nil,
},
{
challengeMethod: "test",
challenge: "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttest",
expected: nil,
},
{
challengeMethod: "test",
challenge: "",
expected: badRequestError(ErrorCodeValidationFailed, InvalidPKCEParamsErrorMessage),
},
{
challengeMethod: "",
challenge: "test",
expected: badRequestError(ErrorCodeValidationFailed, InvalidPKCEParamsErrorMessage),
},
}
for i, c := range cases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
err := validatePKCEParams(c.challengeMethod, c.challenge)
require.Equal(t, c.expected, err)
})
}
}
func TestRequestAud(ts *testing.T) {
mockAPI := API{
config: &conf.GlobalConfiguration{
JWT: conf.JWTConfiguration{
Aud: "authenticated",
Secret: "test-secret",
},
},
}
cases := []struct {
desc string
headers map[string]string
payload map[string]interface{}
expectedAud string
}{
{
desc: "Valid audience slice",
headers: map[string]string{
audHeaderName: "my_custom_aud",
},
payload: map[string]interface{}{
"aud": "authenticated",
},
expectedAud: "my_custom_aud",
},
{
desc: "Valid custom audience",
payload: map[string]interface{}{
"aud": "my_custom_aud",
},
expectedAud: "my_custom_aud",
},
{
desc: "Invalid audience",
payload: map[string]interface{}{
"aud": "",
},
expectedAud: mockAPI.config.JWT.Aud,
},
{
desc: "Missing audience",
payload: map[string]interface{}{
"sub": "d6044b6e-b0ec-4efe-a055-0d2d6ff1dbd8",
},
expectedAud: mockAPI.config.JWT.Aud,
},
}
for _, c := range cases {
ts.Run(c.desc, func(t *testing.T) {
claims := jwt.MapClaims(c.payload)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString([]byte(mockAPI.config.JWT.Secret))
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer: %s", signed))
for k, v := range c.headers {
req.Header.Set(k, v)
}
// set the token in the request context for requestAud
ctx, err := mockAPI.parseJWTClaims(signed, req)
require.NoError(t, err)
aud := mockAPI.requestAud(ctx, req)
require.Equal(t, c.expectedAud, aud)
})
}
}

View File

@ -0,0 +1,405 @@
package api
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net"
"net/http"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/sirupsen/logrus"
standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/hooks"
"github.com/supabase/auth/internal/observability"
"github.com/supabase/auth/internal/storage"
)
const (
DefaultHTTPHookTimeout = 5 * time.Second
DefaultHTTPHookRetries = 3
HTTPHookBackoffDuration = 2 * time.Second
PayloadLimit = 200 * 1024 // 200KB
)
func (a *API) runPostgresHook(ctx context.Context, tx *storage.Connection, hookConfig conf.ExtensibilityPointConfiguration, input, output any) ([]byte, error) {
db := a.db.WithContext(ctx)
request, err := json.Marshal(input)
if err != nil {
panic(err)
}
var response []byte
invokeHookFunc := func(tx *storage.Connection) error {
// We rely on Postgres timeouts to ensure the function doesn't overrun
if terr := tx.RawQuery(fmt.Sprintf("set local statement_timeout TO '%d';", hooks.DefaultTimeout)).Exec(); terr != nil {
return terr
}
if terr := tx.RawQuery(fmt.Sprintf("select %s(?);", hookConfig.HookName), request).First(&response); terr != nil {
return terr
}
// reset the timeout
if terr := tx.RawQuery("set local statement_timeout TO default;").Exec(); terr != nil {
return terr
}
return nil
}
if tx != nil {
if err := invokeHookFunc(tx); err != nil {
return nil, err
}
} else {
if err := db.Transaction(invokeHookFunc); err != nil {
return nil, err
}
}
if err := json.Unmarshal(response, output); err != nil {
return response, err
}
return response, nil
}
func (a *API) runHTTPHook(r *http.Request, hookConfig conf.ExtensibilityPointConfiguration, input any) ([]byte, error) {
ctx := r.Context()
client := http.Client{
Timeout: DefaultHTTPHookTimeout,
}
ctx, cancel := context.WithTimeout(ctx, DefaultHTTPHookTimeout)
defer cancel()
log := observability.GetLogEntry(r).Entry
requestURL := hookConfig.URI
hookLog := log.WithFields(logrus.Fields{
"component": "auth_hook",
"url": requestURL,
})
inputPayload, err := json.Marshal(input)
if err != nil {
return nil, err
}
for i := 0; i < DefaultHTTPHookRetries; i++ {
if i == 0 {
hookLog.Debugf("invocation attempt: %d", i)
} else {
hookLog.Infof("invocation attempt: %d", i)
}
msgID := uuid.Must(uuid.NewV4())
currentTime := time.Now()
signatureList, err := generateSignatures(hookConfig.HTTPHookSecrets, msgID, currentTime, inputPayload)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(inputPayload))
if err != nil {
panic("Failed to make request object")
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("webhook-id", msgID.String())
req.Header.Set("webhook-timestamp", fmt.Sprintf("%d", currentTime.Unix()))
req.Header.Set("webhook-signature", strings.Join(signatureList, ", "))
// By default, Go Client sets encoding to gzip, which does not carry a content length header.
req.Header.Set("Accept-Encoding", "identity")
rsp, err := client.Do(req)
if err != nil && errors.Is(err, context.DeadlineExceeded) {
return nil, unprocessableEntityError(ErrorCodeHookTimeout, fmt.Sprintf("Failed to reach hook within maximum time of %f seconds", DefaultHTTPHookTimeout.Seconds()))
} else if err != nil {
if terr, ok := err.(net.Error); ok && terr.Timeout() || i < DefaultHTTPHookRetries-1 {
hookLog.Errorf("Request timed out for attempt %d with err %s", i, err)
time.Sleep(HTTPHookBackoffDuration)
continue
} else if i == DefaultHTTPHookRetries-1 {
return nil, unprocessableEntityError(ErrorCodeHookTimeoutAfterRetry, "Failed to reach hook after maximum retries")
} else {
return nil, internalServerError("Failed to trigger auth hook, error making HTTP request").WithInternalError(err)
}
}
defer rsp.Body.Close()
switch rsp.StatusCode {
case http.StatusOK, http.StatusNoContent, http.StatusAccepted:
// Header.Get is case insensitive
contentType := rsp.Header.Get("Content-Type")
if contentType == "" {
return nil, badRequestError(ErrorCodeHookPayloadInvalidContentType, "Invalid Content-Type: Missing Content-Type header")
}
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
return nil, badRequestError(ErrorCodeHookPayloadInvalidContentType, fmt.Sprintf("Invalid Content-Type header: %s", err.Error()))
}
if mediaType != "application/json" {
return nil, badRequestError(ErrorCodeHookPayloadInvalidContentType, "Invalid JSON response. Received content-type: "+contentType)
}
if rsp.Body == nil {
return nil, nil
}
limitedReader := io.LimitedReader{R: rsp.Body, N: PayloadLimit}
body, err := io.ReadAll(&limitedReader)
if err != nil {
return nil, err
}
if limitedReader.N <= 0 {
// check if the response body still has excess bytes to be read
if n, _ := rsp.Body.Read(make([]byte, 1)); n > 0 {
return nil, unprocessableEntityError(ErrorCodeHookPayloadOverSizeLimit, fmt.Sprintf("Payload size exceeded size limit of %d bytes", PayloadLimit))
}
}
return body, nil
case http.StatusTooManyRequests, http.StatusServiceUnavailable:
retryAfterHeader := rsp.Header.Get("retry-after")
// Check for truthy values to allow for flexibility to switch to time duration
if retryAfterHeader != "" {
continue
}
return nil, internalServerError("Service currently unavailable due to hook")
case http.StatusBadRequest:
return nil, internalServerError("Invalid payload sent to hook")
case http.StatusUnauthorized:
return nil, internalServerError("Hook requires authorization token")
default:
return nil, internalServerError("Unexpected status code returned from hook: %d", rsp.StatusCode)
}
}
return nil, nil
}
// invokePostgresHook invokes the hook code. conn can be nil, in which case a new
// transaction is opened. If calling invokeHook within a transaction, always
// pass the current transaction, as pool-exhaustion deadlocks are very easy to
// trigger.
func (a *API) invokeHook(conn *storage.Connection, r *http.Request, input, output any) error {
var err error
var response []byte
switch input.(type) {
case *hooks.SendSMSInput:
hookOutput, ok := output.(*hooks.SendSMSOutput)
if !ok {
panic("output should be *hooks.SendSMSOutput")
}
if response, err = a.runHook(r, conn, a.config.Hook.SendSMS, input, output); err != nil {
return err
}
if err := json.Unmarshal(response, hookOutput); err != nil {
return internalServerError("Error unmarshaling Send SMS output.").WithInternalError(err)
}
if hookOutput.IsError() {
httpCode := hookOutput.HookError.HTTPCode
if httpCode == 0 {
httpCode = http.StatusInternalServerError
}
httpError := &HTTPError{
HTTPStatus: httpCode,
Message: hookOutput.HookError.Message,
}
return httpError.WithInternalError(&hookOutput.HookError)
}
return nil
case *hooks.SendEmailInput:
hookOutput, ok := output.(*hooks.SendEmailOutput)
if !ok {
panic("output should be *hooks.SendEmailOutput")
}
if response, err = a.runHook(r, conn, a.config.Hook.SendEmail, input, output); err != nil {
return err
}
if err := json.Unmarshal(response, hookOutput); err != nil {
return internalServerError("Error unmarshaling Send Email output.").WithInternalError(err)
}
if hookOutput.IsError() {
httpCode := hookOutput.HookError.HTTPCode
if httpCode == 0 {
httpCode = http.StatusInternalServerError
}
httpError := &HTTPError{
HTTPStatus: httpCode,
Message: hookOutput.HookError.Message,
}
return httpError.WithInternalError(&hookOutput.HookError)
}
return nil
case *hooks.MFAVerificationAttemptInput:
hookOutput, ok := output.(*hooks.MFAVerificationAttemptOutput)
if !ok {
panic("output should be *hooks.MFAVerificationAttemptOutput")
}
if response, err = a.runHook(r, conn, a.config.Hook.MFAVerificationAttempt, input, output); err != nil {
return err
}
if err := json.Unmarshal(response, hookOutput); err != nil {
return internalServerError("Error unmarshaling MFA Verification Attempt output.").WithInternalError(err)
}
if hookOutput.IsError() {
httpCode := hookOutput.HookError.HTTPCode
if httpCode == 0 {
httpCode = http.StatusInternalServerError
}
httpError := &HTTPError{
HTTPStatus: httpCode,
Message: hookOutput.HookError.Message,
}
return httpError.WithInternalError(&hookOutput.HookError)
}
return nil
case *hooks.PasswordVerificationAttemptInput:
hookOutput, ok := output.(*hooks.PasswordVerificationAttemptOutput)
if !ok {
panic("output should be *hooks.PasswordVerificationAttemptOutput")
}
if response, err = a.runHook(r, conn, a.config.Hook.PasswordVerificationAttempt, input, output); err != nil {
return err
}
if err := json.Unmarshal(response, hookOutput); err != nil {
return internalServerError("Error unmarshaling Password Verification Attempt output.").WithInternalError(err)
}
if hookOutput.IsError() {
httpCode := hookOutput.HookError.HTTPCode
if httpCode == 0 {
httpCode = http.StatusInternalServerError
}
httpError := &HTTPError{
HTTPStatus: httpCode,
Message: hookOutput.HookError.Message,
}
return httpError.WithInternalError(&hookOutput.HookError)
}
return nil
case *hooks.CustomAccessTokenInput:
hookOutput, ok := output.(*hooks.CustomAccessTokenOutput)
if !ok {
panic("output should be *hooks.CustomAccessTokenOutput")
}
if response, err = a.runHook(r, conn, a.config.Hook.CustomAccessToken, input, output); err != nil {
return err
}
if err := json.Unmarshal(response, hookOutput); err != nil {
return internalServerError("Error unmarshaling Custom Access Token output.").WithInternalError(err)
}
if hookOutput.IsError() {
httpCode := hookOutput.HookError.HTTPCode
if httpCode == 0 {
httpCode = http.StatusInternalServerError
}
httpError := &HTTPError{
HTTPStatus: httpCode,
Message: hookOutput.HookError.Message,
}
return httpError.WithInternalError(&hookOutput.HookError)
}
if err := validateTokenClaims(hookOutput.Claims); err != nil {
httpCode := hookOutput.HookError.HTTPCode
if httpCode == 0 {
httpCode = http.StatusInternalServerError
}
httpError := &HTTPError{
HTTPStatus: httpCode,
Message: err.Error(),
}
return httpError
}
return nil
}
return nil
}
func (a *API) runHook(r *http.Request, conn *storage.Connection, hookConfig conf.ExtensibilityPointConfiguration, input, output any) ([]byte, error) {
ctx := r.Context()
logEntry := observability.GetLogEntry(r)
hookStart := time.Now()
var response []byte
var err error
switch {
case strings.HasPrefix(hookConfig.URI, "http:") || strings.HasPrefix(hookConfig.URI, "https:"):
response, err = a.runHTTPHook(r, hookConfig, input)
case strings.HasPrefix(hookConfig.URI, "pg-functions:"):
response, err = a.runPostgresHook(ctx, conn, hookConfig, input, output)
default:
return nil, fmt.Errorf("unsupported protocol: %q only postgres hooks and HTTPS functions are supported at the moment", hookConfig.URI)
}
duration := time.Since(hookStart)
if err != nil {
logEntry.Entry.WithFields(logrus.Fields{
"action": "run_hook",
"hook": hookConfig.URI,
"success": false,
"duration": duration.Microseconds(),
}).WithError(err).Warn("Hook errored out")
return nil, internalServerError("Error running hook URI: %v", hookConfig.URI).WithInternalError(err)
}
logEntry.Entry.WithFields(logrus.Fields{
"action": "run_hook",
"hook": hookConfig.URI,
"success": true,
"duration": duration.Microseconds(),
}).WithError(err).Info("Hook ran successfully")
return response, nil
}
func generateSignatures(secrets []string, msgID uuid.UUID, currentTime time.Time, inputPayload []byte) ([]string, error) {
SymmetricSignaturePrefix := "v1,"
// TODO(joel): Handle asymmetric case once library has been upgraded
var signatureList []string
for _, secret := range secrets {
if strings.HasPrefix(secret, SymmetricSignaturePrefix) {
trimmedSecret := strings.TrimPrefix(secret, SymmetricSignaturePrefix)
wh, err := standardwebhooks.NewWebhook(trimmedSecret)
if err != nil {
return nil, err
}
signature, err := wh.Sign(msgID.String(), currentTime, inputPayload)
if err != nil {
return nil, err
}
signatureList = append(signatureList, signature)
} else {
return nil, errors.New("invalid signature format")
}
}
return signatureList, nil
}

View File

@ -0,0 +1,287 @@
package api
import (
"encoding/json"
"net/http"
"testing"
"net/http/httptest"
"github.com/pkg/errors"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/hooks"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/storage"
"gopkg.in/h2non/gock.v1"
)
var handleApiRequest func(*http.Request) (*http.Response, error)
type HooksTestSuite struct {
suite.Suite
API *API
Config *conf.GlobalConfiguration
TestUser *models.User
}
type MockHttpClient struct {
mock.Mock
}
func (m *MockHttpClient) Do(req *http.Request) (*http.Response, error) {
return handleApiRequest(req)
}
func TestHooks(t *testing.T) {
api, config, err := setupAPIForTest()
require.NoError(t, err)
ts := &HooksTestSuite{
API: api,
Config: config,
}
defer api.db.Close()
suite.Run(t, ts)
}
func (ts *HooksTestSuite) SetupTest() {
models.TruncateAll(ts.API.db)
u, err := models.NewUser("123456789", "testemail@gmail.com", "securetestpassword", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error creating test user model")
require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user")
ts.TestUser = u
}
func (ts *HooksTestSuite) TestRunHTTPHook() {
// setup mock requests for hooks
defer gock.OffAll()
input := hooks.SendSMSInput{
User: ts.TestUser,
SMS: hooks.SMS{
OTP: "123456",
},
}
testURL := "http://localhost:54321/functions/v1/custom-sms-sender"
ts.Config.Hook.SendSMS.URI = testURL
unsuccessfulResponse := hooks.AuthHookError{
HTTPCode: http.StatusUnprocessableEntity,
Message: "test error",
}
testCases := []struct {
description string
expectError bool
mockResponse hooks.AuthHookError
}{
{
description: "Hook returns success",
expectError: false,
mockResponse: hooks.AuthHookError{},
},
{
description: "Hook returns error",
expectError: true,
mockResponse: unsuccessfulResponse,
},
}
gock.New(ts.Config.Hook.SendSMS.URI).
Post("/").
MatchType("json").
Reply(http.StatusOK).
JSON(hooks.SendSMSOutput{})
gock.New(ts.Config.Hook.SendSMS.URI).
Post("/").
MatchType("json").
Reply(http.StatusUnprocessableEntity).
JSON(hooks.SendSMSOutput{HookError: unsuccessfulResponse})
for _, tc := range testCases {
ts.Run(tc.description, func() {
req, _ := http.NewRequest("POST", ts.Config.Hook.SendSMS.URI, nil)
body, err := ts.API.runHTTPHook(req, ts.Config.Hook.SendSMS, &input)
if !tc.expectError {
require.NoError(ts.T(), err)
} else {
require.Error(ts.T(), err)
if body != nil {
var output hooks.SendSMSOutput
require.NoError(ts.T(), json.Unmarshal(body, &output))
require.Equal(ts.T(), unsuccessfulResponse.HTTPCode, output.HookError.HTTPCode)
require.Equal(ts.T(), unsuccessfulResponse.Message, output.HookError.Message)
}
}
})
}
require.True(ts.T(), gock.IsDone())
}
func (ts *HooksTestSuite) TestShouldRetryWithRetryAfterHeader() {
defer gock.OffAll()
input := hooks.SendSMSInput{
User: ts.TestUser,
SMS: hooks.SMS{
OTP: "123456",
},
}
testURL := "http://localhost:54321/functions/v1/custom-sms-sender"
ts.Config.Hook.SendSMS.URI = testURL
gock.New(testURL).
Post("/").
MatchType("json").
Reply(http.StatusTooManyRequests).
SetHeader("retry-after", "true").SetHeader("content-type", "application/json")
// Simulate an additional response for the retry attempt
gock.New(testURL).
Post("/").
MatchType("json").
Reply(http.StatusOK).
JSON(hooks.SendSMSOutput{}).SetHeader("content-type", "application/json")
// Simulate the original HTTP request which triggered the hook
req, err := http.NewRequest("POST", "http://localhost:9998/otp", nil)
require.NoError(ts.T(), err)
body, err := ts.API.runHTTPHook(req, ts.Config.Hook.SendSMS, &input)
require.NoError(ts.T(), err)
var output hooks.SendSMSOutput
err = json.Unmarshal(body, &output)
require.NoError(ts.T(), err, "Unmarshal should not fail")
// Ensure that all expected HTTP interactions (mocks) have been called
require.True(ts.T(), gock.IsDone(), "Expected all mocks to have been called including retry")
}
func (ts *HooksTestSuite) TestShouldReturnErrorForNonJSONContentType() {
defer gock.OffAll()
input := hooks.SendSMSInput{
User: ts.TestUser,
SMS: hooks.SMS{
OTP: "123456",
},
}
testURL := "http://localhost:54321/functions/v1/custom-sms-sender"
ts.Config.Hook.SendSMS.URI = testURL
gock.New(testURL).
Post("/").
MatchType("json").
Reply(http.StatusOK).
SetHeader("content-type", "text/plain")
req, err := http.NewRequest("POST", "http://localhost:9999/otp", nil)
require.NoError(ts.T(), err)
_, err = ts.API.runHTTPHook(req, ts.Config.Hook.SendSMS, &input)
require.Error(ts.T(), err, "Expected an error due to wrong content type")
require.Contains(ts.T(), err.Error(), "Invalid JSON response.")
require.True(ts.T(), gock.IsDone(), "Expected all mocks to have been called")
}
func (ts *HooksTestSuite) TestInvokeHookIntegration() {
// We use the Send Email Hook as illustration
defer gock.OffAll()
hookFunctionSQL := `
create or replace function invoke_test(input jsonb)
returns json as $$
begin
return input;
end; $$ language plpgsql;`
require.NoError(ts.T(), ts.API.db.RawQuery(hookFunctionSQL).Exec())
testHTTPUri := "http://myauthservice.com/signup"
testHTTPSUri := "https://myauthservice.com/signup"
testPGUri := "pg-functions://postgres/auth/invoke_test"
successOutput := map[string]interface{}{}
authEndpoint := "https://app.myapp.com/otp"
gock.New(testHTTPUri).
Post("/").
MatchType("json").
Reply(http.StatusOK).
JSON(successOutput).SetHeader("content-type", "application/json")
gock.New(testHTTPSUri).
Post("/").
MatchType("json").
Reply(http.StatusOK).
JSON(successOutput).SetHeader("content-type", "application/json")
tests := []struct {
description string
conn *storage.Connection
request *http.Request
input any
output any
uri string
expectedError error
}{
{
description: "HTTP endpoint success",
conn: nil,
request: httptest.NewRequest("POST", authEndpoint, nil),
input: &hooks.SendEmailInput{},
output: &hooks.SendEmailOutput{},
uri: testHTTPUri,
},
{
description: "HTTPS endpoint success",
conn: nil,
request: httptest.NewRequest("POST", authEndpoint, nil),
input: &hooks.SendEmailInput{},
output: &hooks.SendEmailOutput{},
uri: testHTTPSUri,
},
{
description: "PostgreSQL function success",
conn: ts.API.db,
request: httptest.NewRequest("POST", authEndpoint, nil),
input: &hooks.SendEmailInput{},
output: &hooks.SendEmailOutput{},
uri: testPGUri,
},
{
description: "Unsupported protocol error",
conn: nil,
request: httptest.NewRequest("POST", authEndpoint, nil),
input: &hooks.SendEmailInput{},
output: &hooks.SendEmailOutput{},
uri: "ftp://example.com/path",
expectedError: errors.New("unsupported protocol: \"ftp://example.com/path\" only postgres hooks and HTTPS functions are supported at the moment"),
},
}
var err error
for _, tc := range tests {
// Set up hook config
ts.Config.Hook.SendEmail.Enabled = true
ts.Config.Hook.SendEmail.URI = tc.uri
require.NoError(ts.T(), ts.Config.Hook.SendEmail.PopulateExtensibilityPoint())
ts.Run(tc.description, func() {
err = ts.API.invokeHook(tc.conn, tc.request, tc.input, tc.output)
if tc.expectedError != nil {
require.EqualError(ts.T(), err, tc.expectedError.Error())
} else {
require.NoError(ts.T(), err)
}
})
}
// Ensure that all expected HTTP interactions (mocks) have been called
require.True(ts.T(), gock.IsDone(), "Expected all mocks to have been called including retry")
}

View File

@ -0,0 +1,155 @@
package api
import (
"context"
"net/http"
"github.com/fatih/structs"
"github.com/go-chi/chi/v5"
"github.com/gofrs/uuid"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/storage"
)
func (a *API) DeleteIdentity(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
claims := getClaims(ctx)
if claims == nil {
return internalServerError("Could not read claims")
}
identityID, err := uuid.FromString(chi.URLParam(r, "identity_id"))
if err != nil {
return notFoundError(ErrorCodeValidationFailed, "identity_id must be an UUID")
}
aud := a.requestAud(ctx, r)
audienceFromClaims, _ := claims.GetAudience()
if len(audienceFromClaims) == 0 || aud != audienceFromClaims[0] {
return forbiddenError(ErrorCodeUnexpectedAudience, "Token audience doesn't match request audience")
}
user := getUser(ctx)
if len(user.Identities) <= 1 {
return unprocessableEntityError(ErrorCodeSingleIdentityNotDeletable, "User must have at least 1 identity after unlinking")
}
var identityToBeDeleted *models.Identity
for i := range user.Identities {
identity := user.Identities[i]
if identity.ID == identityID {
identityToBeDeleted = &identity
break
}
}
if identityToBeDeleted == nil {
return unprocessableEntityError(ErrorCodeIdentityNotFound, "Identity doesn't exist")
}
err = a.db.Transaction(func(tx *storage.Connection) error {
if terr := models.NewAuditLogEntry(r, tx, user, models.IdentityUnlinkAction, "", map[string]interface{}{
"identity_id": identityToBeDeleted.ID,
"provider": identityToBeDeleted.Provider,
"provider_id": identityToBeDeleted.ProviderID,
}); terr != nil {
return internalServerError("Error recording audit log entry").WithInternalError(terr)
}
if terr := tx.Destroy(identityToBeDeleted); terr != nil {
return internalServerError("Database error deleting identity").WithInternalError(terr)
}
switch identityToBeDeleted.Provider {
case "phone":
user.PhoneConfirmedAt = nil
if terr := user.SetPhone(tx, ""); terr != nil {
return internalServerError("Database error updating user phone").WithInternalError(terr)
}
if terr := tx.UpdateOnly(user, "phone_confirmed_at"); terr != nil {
return internalServerError("Database error updating user phone").WithInternalError(terr)
}
default:
if terr := user.UpdateUserEmailFromIdentities(tx); terr != nil {
if models.IsUniqueConstraintViolatedError(terr) {
return unprocessableEntityError(ErrorCodeEmailConflictIdentityNotDeletable, "Unable to unlink identity due to email conflict").WithInternalError(terr)
}
return internalServerError("Database error updating user email").WithInternalError(terr)
}
}
if terr := user.UpdateAppMetaDataProviders(tx); terr != nil {
return internalServerError("Database error updating user providers").WithInternalError(terr)
}
return nil
})
if err != nil {
return err
}
return sendJSON(w, http.StatusOK, map[string]interface{}{})
}
func (a *API) LinkIdentity(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
user := getUser(ctx)
rurl, err := a.GetExternalProviderRedirectURL(w, r, user)
if err != nil {
return err
}
skipHTTPRedirect := r.URL.Query().Get("skip_http_redirect") == "true"
if skipHTTPRedirect {
return sendJSON(w, http.StatusOK, map[string]interface{}{
"url": rurl,
})
}
http.Redirect(w, r, rurl, http.StatusFound)
return nil
}
func (a *API) linkIdentityToUser(r *http.Request, ctx context.Context, tx *storage.Connection, userData *provider.UserProvidedData, providerType string) (*models.User, error) {
targetUser := getTargetUser(ctx)
identity, terr := models.FindIdentityByIdAndProvider(tx, userData.Metadata.Subject, providerType)
if terr != nil {
if !models.IsNotFoundError(terr) {
return nil, internalServerError("Database error finding identity for linking").WithInternalError(terr)
}
}
if identity != nil {
if identity.UserID == targetUser.ID {
return nil, unprocessableEntityError(ErrorCodeIdentityAlreadyExists, "Identity is already linked")
}
return nil, unprocessableEntityError(ErrorCodeIdentityAlreadyExists, "Identity is already linked to another user")
}
if _, terr := a.createNewIdentity(tx, targetUser, providerType, structs.Map(userData.Metadata)); terr != nil {
return nil, terr
}
if targetUser.GetEmail() == "" {
if terr := targetUser.UpdateUserEmailFromIdentities(tx); terr != nil {
if models.IsUniqueConstraintViolatedError(terr) {
return nil, badRequestError(ErrorCodeEmailExists, DuplicateEmailMsg)
}
return nil, terr
}
if !userData.Metadata.EmailVerified {
if terr := a.sendConfirmation(r, tx, targetUser, models.ImplicitFlow); terr != nil {
return nil, terr
}
return nil, storage.NewCommitWithError(unprocessableEntityError(ErrorCodeEmailNotConfirmed, "Unverified email with %v. A confirmation email has been sent to your %v email", providerType, providerType))
}
if terr := targetUser.Confirm(tx); terr != nil {
return nil, terr
}
if targetUser.IsAnonymous {
targetUser.IsAnonymous = false
if terr := tx.UpdateOnly(targetUser, "is_anonymous"); terr != nil {
return nil, terr
}
}
}
if terr := targetUser.UpdateAppMetaDataProviders(tx); terr != nil {
return nil, terr
}
return targetUser, nil
}

View File

@ -0,0 +1,227 @@
package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/models"
)
type IdentityTestSuite struct {
suite.Suite
API *API
Config *conf.GlobalConfiguration
}
func TestIdentity(t *testing.T) {
api, config, err := setupAPIForTest()
require.NoError(t, err)
ts := &IdentityTestSuite{
API: api,
Config: config,
}
defer api.db.Close()
suite.Run(t, ts)
}
func (ts *IdentityTestSuite) SetupTest() {
models.TruncateAll(ts.API.db)
// Create user
u, err := models.NewUser("", "one@example.com", "password", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error creating test user model")
require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user")
require.NoError(ts.T(), u.Confirm(ts.API.db))
// Create identity
i, err := models.NewIdentity(u, "email", map[string]interface{}{
"sub": u.ID.String(),
"email": u.GetEmail(),
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(i))
// Create user with 2 identities
u, err = models.NewUser("123456789", "two@example.com", "password", ts.Config.JWT.Aud, nil)
require.NoError(ts.T(), err, "Error creating test user model")
require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user")
require.NoError(ts.T(), u.Confirm(ts.API.db))
require.NoError(ts.T(), u.ConfirmPhone(ts.API.db))
i, err = models.NewIdentity(u, "email", map[string]interface{}{
"sub": u.ID.String(),
"email": u.GetEmail(),
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(i))
i2, err := models.NewIdentity(u, "phone", map[string]interface{}{
"sub": u.ID.String(),
"phone": u.GetPhone(),
})
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(i2))
}
func (ts *IdentityTestSuite) TestLinkIdentityToUser() {
u, err := models.FindUserByEmailAndAudience(ts.API.db, "one@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
ctx := withTargetUser(context.Background(), u)
// link a valid identity
testValidUserData := &provider.UserProvidedData{
Metadata: &provider.Claims{
Subject: "test_subject",
},
}
// request is just used as a placeholder in the function
r := httptest.NewRequest(http.MethodGet, "/identities", nil)
u, err = ts.API.linkIdentityToUser(r, ctx, ts.API.db, testValidUserData, "test")
require.NoError(ts.T(), err)
// load associated identities for the user
ts.API.db.Load(u, "Identities")
require.Len(ts.T(), u.Identities, 2)
require.Equal(ts.T(), u.AppMetaData["provider"], "email")
require.Equal(ts.T(), u.AppMetaData["providers"], []string{"email", "test"})
// link an already existing identity
testExistingUserData := &provider.UserProvidedData{
Metadata: &provider.Claims{
Subject: u.ID.String(),
},
}
u, err = ts.API.linkIdentityToUser(r, ctx, ts.API.db, testExistingUserData, "email")
require.ErrorIs(ts.T(), err, unprocessableEntityError(ErrorCodeIdentityAlreadyExists, "Identity is already linked"))
require.Nil(ts.T(), u)
}
func (ts *IdentityTestSuite) TestUnlinkIdentityError() {
ts.Config.Security.ManualLinkingEnabled = true
userWithOneIdentity, err := models.FindUserByEmailAndAudience(ts.API.db, "one@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
userWithTwoIdentities, err := models.FindUserByEmailAndAudience(ts.API.db, "two@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
cases := []struct {
desc string
user *models.User
identityId uuid.UUID
expectedError *HTTPError
}{
{
desc: "User must have at least 1 identity after unlinking",
user: userWithOneIdentity,
identityId: userWithOneIdentity.Identities[0].ID,
expectedError: unprocessableEntityError(ErrorCodeSingleIdentityNotDeletable, "User must have at least 1 identity after unlinking"),
},
{
desc: "Identity doesn't exist",
user: userWithTwoIdentities,
identityId: uuid.Must(uuid.NewV4()),
expectedError: unprocessableEntityError(ErrorCodeIdentityNotFound, "Identity doesn't exist"),
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
token := ts.generateAccessTokenAndSession(c.user)
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/user/identities/%s", c.identityId), nil)
require.NoError(ts.T(), err)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), c.expectedError.HTTPStatus, w.Code)
var data HTTPError
require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
require.Equal(ts.T(), c.expectedError.Message, data.Message)
})
}
}
func (ts *IdentityTestSuite) TestUnlinkIdentity() {
ts.Config.Security.ManualLinkingEnabled = true
// we want to test 2 cases here: unlinking a phone identity and email identity from a user
cases := []struct {
desc string
// the provider to be unlinked
provider string
// the remaining provider that should be linked to the user
providerRemaining string
}{
{
desc: "Unlink phone identity successfully",
provider: "phone",
providerRemaining: "email",
},
{
desc: "Unlink email identity successfully",
provider: "email",
providerRemaining: "phone",
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
// teardown and reset the state of the db to prevent running into errors
ts.SetupTest()
u, err := models.FindUserByEmailAndAudience(ts.API.db, "two@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
identity, err := models.FindIdentityByIdAndProvider(ts.API.db, u.ID.String(), c.provider)
require.NoError(ts.T(), err)
token := ts.generateAccessTokenAndSession(u)
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/user/identities/%s", identity.ID), nil)
require.NoError(ts.T(), err)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
require.Equal(ts.T(), http.StatusOK, w.Code)
// sanity checks
u, err = models.FindUserByID(ts.API.db, u.ID)
require.NoError(ts.T(), err)
require.Len(ts.T(), u.Identities, 1)
require.Equal(ts.T(), u.Identities[0].Provider, c.providerRemaining)
// conditional checks depending on the provider that was unlinked
switch c.provider {
case "phone":
require.Equal(ts.T(), "", u.GetPhone())
require.Nil(ts.T(), u.PhoneConfirmedAt)
case "email":
require.Equal(ts.T(), "", u.GetEmail())
require.Nil(ts.T(), u.EmailConfirmedAt)
}
// user still has a phone / email identity linked so it should not be unconfirmed
require.NotNil(ts.T(), u.ConfirmedAt)
})
}
}
func (ts *IdentityTestSuite) generateAccessTokenAndSession(u *models.User) string {
s, err := models.NewSession(u.ID, nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(s))
req := httptest.NewRequest(http.MethodPost, "/token?grant_type=password", nil)
token, _, err := ts.API.generateAccessToken(req, ts.API.db, u, &s.ID, models.PasswordGrant)
require.NoError(ts.T(), err)
return token
}

View File

@ -0,0 +1,92 @@
package api
import (
"net/http"
"github.com/fatih/structs"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/storage"
)
// InviteParams are the parameters the Signup endpoint accepts
type InviteParams struct {
Email string `json:"email"`
Data map[string]interface{} `json:"data"`
}
// Invite is the endpoint for inviting a new user
func (a *API) Invite(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)
adminUser := getAdminUser(ctx)
params := &InviteParams{}
if err := retrieveRequestParams(r, params); err != nil {
return err
}
var err error
params.Email, err = a.validateEmail(params.Email)
if err != nil {
return err
}
aud := a.requestAud(ctx, r)
user, err := models.FindUserByEmailAndAudience(db, params.Email, aud)
if err != nil && !models.IsNotFoundError(err) {
return internalServerError("Database error finding user").WithInternalError(err)
}
err = db.Transaction(func(tx *storage.Connection) error {
if user != nil {
if user.IsConfirmed() {
return unprocessableEntityError(ErrorCodeEmailExists, DuplicateEmailMsg)
}
} else {
signupParams := SignupParams{
Email: params.Email,
Data: params.Data,
Aud: aud,
Provider: "email",
}
// because params above sets no password, this method
// is not computationally hard so it can be used within
// a database transaction
user, err = signupParams.ToUserModel(false /* <- isSSOUser */)
if err != nil {
return err
}
user, err = a.signupNewUser(tx, user)
if err != nil {
return err
}
identity, err := a.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{
Subject: user.ID.String(),
Email: user.GetEmail(),
}))
if err != nil {
return err
}
user.Identities = []models.Identity{*identity}
}
if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UserInvitedAction, "", map[string]interface{}{
"user_id": user.ID,
"user_email": user.Email,
}); terr != nil {
return terr
}
if err := a.sendInvite(r, tx, user); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return sendJSON(w, http.StatusOK, user)
}

View File

@ -0,0 +1,404 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/crypto"
"github.com/supabase/auth/internal/models"
)
type InviteTestSuite struct {
suite.Suite
API *API
Config *conf.GlobalConfiguration
token string
}
func TestInvite(t *testing.T) {
api, config, err := setupAPIForTest()
require.NoError(t, err)
ts := &InviteTestSuite{
API: api,
Config: config,
}
defer api.db.Close()
suite.Run(t, ts)
}
func (ts *InviteTestSuite) SetupTest() {
models.TruncateAll(ts.API.db)
// Setup response recorder with super admin privileges
ts.token = ts.makeSuperAdmin("")
}
func (ts *InviteTestSuite) makeSuperAdmin(email string) string {
// Cleanup existing user, if they already exist
if u, _ := models.FindUserByEmailAndAudience(ts.API.db, email, ts.Config.JWT.Aud); u != nil {
require.NoError(ts.T(), ts.API.db.Destroy(u), "Error deleting user")
}
u, err := models.NewUser("123456789", email, "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test User"})
require.NoError(ts.T(), err, "Error making new user")
require.NoError(ts.T(), ts.API.db.Create(u))
u.Role = "supabase_admin"
var token string
session, err := models.NewSession(u.ID, nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(session))
req := httptest.NewRequest(http.MethodPost, "/invite", nil)
token, _, err = ts.API.generateAccessToken(req, ts.API.db, u, &session.ID, models.Invite)
require.NoError(ts.T(), err, "Error generating access token")
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}))
_, err = p.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
require.NoError(ts.T(), err, "Error parsing token")
return token
}
func (ts *InviteTestSuite) TestInvite() {
// Request body
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"email": "test@example.com",
"data": map[string]interface{}{
"a": 1,
},
}))
// Setup request
req := httptest.NewRequest(http.MethodPost, "http://localhost/invite", &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
// Setup response recorder
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusOK, w.Code)
}
func (ts *InviteTestSuite) TestInviteAfterSignupShouldNotReturnSensitiveFields() {
// To allow us to send signup and invite request in succession
ts.Config.SMTP.MaxFrequency = 5
// Request body
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"email": "test@example.com",
"data": map[string]interface{}{
"a": 1,
},
}))
// Setup request
req := httptest.NewRequest(http.MethodPost, "http://localhost/invite", &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
// Setup response recorder
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusOK, w.Code)
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"email": "test@example.com",
"password": "test123",
"data": map[string]interface{}{
"a": 1,
},
}))
// Setup request
req = httptest.NewRequest(http.MethodPost, "/signup", &buffer)
req.Header.Set("Content-Type", "application/json")
// Setup response recorder
x := httptest.NewRecorder()
ts.API.handler.ServeHTTP(x, req)
require.Equal(ts.T(), http.StatusOK, x.Code)
data := models.User{}
require.NoError(ts.T(), json.NewDecoder(x.Body).Decode(&data))
// Sensitive fields
require.Equal(ts.T(), 0, len(data.Identities))
require.Equal(ts.T(), 0, len(data.UserMetaData))
}
func (ts *InviteTestSuite) TestInvite_WithoutAccess() {
// Request body
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"email": "test@example.com",
"data": map[string]interface{}{
"a": 1,
},
}))
// Setup request
req := httptest.NewRequest(http.MethodPost, "http://localhost/invite", &buffer)
req.Header.Set("Content-Type", "application/json")
// Setup response recorder
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusUnauthorized, w.Code) // 401 OK because the invite request above has no Authorization header
}
func (ts *InviteTestSuite) TestVerifyInvite() {
cases := []struct {
desc string
email string
requestBody map[string]interface{}
expected int
}{
{
"Verify invite with password",
"test@example.com",
map[string]interface{}{
"email": "test@example.com",
"type": "invite",
"token": "asdf",
"password": "testing",
},
http.StatusOK,
},
{
"Verify invite with no password",
"test1@example.com",
map[string]interface{}{
"email": "test1@example.com",
"type": "invite",
"token": "asdf",
},
http.StatusOK,
},
}
for _, c := range cases {
ts.Run(c.desc, func() {
user, err := models.NewUser("", c.email, "", ts.Config.JWT.Aud, nil)
now := time.Now()
user.InvitedAt = &now
user.ConfirmationSentAt = &now
user.EncryptedPassword = nil
user.ConfirmationToken = crypto.GenerateTokenHash(c.email, c.requestBody["token"].(string))
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(user))
require.NoError(ts.T(), models.CreateOneTimeToken(ts.API.db, user.ID, user.GetEmail(), user.ConfirmationToken, models.ConfirmationToken))
// Find test user
_, err = models.FindUserByEmailAndAudience(ts.API.db, c.email, ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
// Request body
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.requestBody))
// Setup request
req := httptest.NewRequest(http.MethodPost, "http://localhost/verify", &buffer)
req.Header.Set("Content-Type", "application/json")
// Setup response recorder
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), c.expected, w.Code, w.Body.String())
})
}
}
func (ts *InviteTestSuite) TestInviteExternalGitlab() {
tokenCount, userCount := 0, 0
code := "authcode"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/oauth/token":
tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Gitlab.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"gitlab_token","expires_in":100000}`)
case "/api/v4/user":
userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"name":"Gitlab Test","email":"gitlab@example.com","avatar_url":"http://example.com/avatar","confirmed_at": "2020-01-01T00:00:00.000Z"}`)
case "/api/v4/user/emails":
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `[]`)
default:
w.WriteHeader(http.StatusInternalServerError)
ts.Fail("unknown gitlab oauth call %s", r.URL.Path)
}
}))
defer server.Close()
ts.Config.External.Gitlab.URL = server.URL
// invite user
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(InviteParams{
Email: "gitlab@example.com",
}))
req := httptest.NewRequest(http.MethodPost, "http://localhost/invite", &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusOK, w.Code)
// Find test user
user, err := models.FindUserByEmailAndAudience(ts.API.db, "gitlab@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
// get redirect url w/ state
req = httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=gitlab&invite_token="+user.ConfirmationToken, nil)
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
state := q.Get("state")
// auth server callback
testURL, err := url.Parse("http://localhost/callback")
ts.Require().NoError(err)
v := testURL.Query()
v.Set("code", code)
v.Set("state", state)
testURL.RawQuery = v.Encode()
req = httptest.NewRequest(http.MethodGet, testURL.String(), nil)
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err = url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
// ensure redirect has #access_token=...
v, err = url.ParseQuery(u.Fragment)
ts.Require().NoError(err)
ts.Require().Empty(v.Get("error_description"))
ts.Require().Empty(v.Get("error"))
ts.NotEmpty(v.Get("access_token"))
ts.NotEmpty(v.Get("refresh_token"))
ts.NotEmpty(v.Get("expires_in"))
ts.Equal("bearer", v.Get("token_type"))
ts.Equal(1, tokenCount)
ts.Equal(1, userCount)
// ensure user has been created with metadata
user, err = models.FindUserByEmailAndAudience(ts.API.db, "gitlab@example.com", ts.Config.JWT.Aud)
ts.Require().NoError(err)
ts.Equal("Gitlab Test", user.UserMetaData["full_name"])
ts.Equal("http://example.com/avatar", user.UserMetaData["avatar_url"])
ts.Equal("gitlab", user.AppMetaData["provider"])
ts.Equal([]interface{}{"gitlab"}, user.AppMetaData["providers"])
}
func (ts *InviteTestSuite) TestInviteExternalGitlab_MismatchedEmails() {
tokenCount, userCount := 0, 0
code := "authcode"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/oauth/token":
tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Gitlab.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"gitlab_token","expires_in":100000}`)
case "/api/v4/user":
userCount++
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"name":"Gitlab Test","email":"gitlab+mismatch@example.com","avatar_url":"http://example.com/avatar","confirmed_at": "2020-01-01T00:00:00.000Z"}`)
case "/api/v4/user/emails":
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `[]`)
default:
w.WriteHeader(500)
ts.Fail("unknown gitlab oauth call %s", r.URL.Path)
}
}))
defer server.Close()
ts.Config.External.Gitlab.URL = server.URL
// invite user
var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(InviteParams{
Email: "gitlab@example.com",
}))
req := httptest.NewRequest(http.MethodPost, "http://localhost/invite", &buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusOK, w.Code)
// Find test user
user, err := models.FindUserByEmailAndAudience(ts.API.db, "gitlab@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
// get redirect url w/ state
req = httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=gitlab&invite_token="+user.ConfirmationToken, nil)
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
state := q.Get("state")
// auth server callback
testURL, err := url.Parse("http://localhost/callback")
ts.Require().NoError(err)
v := testURL.Query()
v.Set("code", code)
v.Set("state", state)
testURL.RawQuery = v.Encode()
req = httptest.NewRequest(http.MethodGet, testURL.String(), nil)
w = httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err = url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
// ensure redirect has #access_token=...
v, err = url.ParseQuery(u.RawQuery)
ts.Require().NoError(err, u.RawQuery)
ts.Require().NotEmpty(v.Get("error_description"))
ts.Require().Equal("invalid_request", v.Get("error"))
}

View File

@ -0,0 +1,61 @@
package api
import (
"net/http"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/lestrrat-go/jwx/v2/jwa"
jwk "github.com/lestrrat-go/jwx/v2/jwk"
"github.com/supabase/auth/internal/conf"
)
type JwksResponse struct {
Keys []jwk.Key `json:"keys"`
}
func (a *API) Jwks(w http.ResponseWriter, r *http.Request) error {
config := a.config
resp := JwksResponse{
Keys: []jwk.Key{},
}
for _, key := range config.JWT.Keys {
// don't expose hmac jwk in endpoint
if key.PublicKey == nil || key.PublicKey.KeyType() == jwa.OctetSeq {
continue
}
resp.Keys = append(resp.Keys, key.PublicKey)
}
w.Header().Set("Cache-Control", "public, max-age=600")
return sendJSON(w, http.StatusOK, resp)
}
func signJwt(config *conf.JWTConfiguration, claims jwt.Claims) (string, error) {
signingJwk, err := conf.GetSigningJwk(config)
if err != nil {
return "", err
}
signingMethod := conf.GetSigningAlg(signingJwk)
token := jwt.NewWithClaims(signingMethod, claims)
if token.Header == nil {
token.Header = make(map[string]interface{})
}
if _, ok := token.Header["kid"]; !ok {
if kid := signingJwk.KeyID(); kid != "" {
token.Header["kid"] = kid
}
}
// this serializes the aud claim to a string
jwt.MarshalSingleStringAsArray = false
signingKey, err := conf.GetSigningKey(signingJwk)
if err != nil {
return "", err
}
signed, err := token.SignedString(signingKey)
if err != nil {
return "", err
}
return signed, nil
}

View File

@ -0,0 +1,79 @@
package api
import (
"crypto/rand"
"crypto/rsa"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/conf"
)
func TestJwks(t *testing.T) {
// generate RSA key pair for testing
rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
rsaJwkPrivate, err := jwk.FromRaw(rsaPrivateKey)
require.NoError(t, err)
rsaJwkPublic, err := rsaJwkPrivate.PublicKey()
require.NoError(t, err)
kid := rsaJwkPublic.KeyID()
cases := []struct {
desc string
config conf.JWTConfiguration
expectedLen int
}{
{
desc: "hmac key should not be returned",
config: conf.JWTConfiguration{
Aud: "authenticated",
Secret: "test-secret",
},
expectedLen: 0,
},
{
desc: "rsa public key returned",
config: conf.JWTConfiguration{
Aud: "authenticated",
Secret: "test-secret",
Keys: conf.JwtKeysDecoder{
kid: conf.JwkInfo{
PublicKey: rsaJwkPublic,
PrivateKey: rsaJwkPrivate,
},
},
},
expectedLen: 1,
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
mockAPI, _, err := setupAPIForTest()
require.NoError(t, err)
mockAPI.config.JWT = c.config
req := httptest.NewRequest(http.MethodGet, "/.well-known/jwks.json", nil)
w := httptest.NewRecorder()
mockAPI.handler.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var data map[string]interface{}
require.NoError(t, json.NewDecoder(w.Body).Decode(&data))
require.Len(t, data["keys"], c.expectedLen)
for _, key := range data["keys"].([]interface{}) {
bytes, err := json.Marshal(key)
require.NoError(t, err)
actualKey, err := jwk.ParseKey(bytes)
require.NoError(t, err)
require.Equal(t, c.config.Keys[kid].PublicKey, actualKey)
}
})
}
}

View File

@ -0,0 +1,73 @@
package api
import (
"fmt"
"net/http"
"github.com/sirupsen/logrus"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/storage"
)
type LogoutBehavior string
const (
LogoutGlobal LogoutBehavior = "global"
LogoutLocal LogoutBehavior = "local"
LogoutOthers LogoutBehavior = "others"
)
// Logout is the endpoint for logging out a user and thereby revoking any refresh tokens
func (a *API) Logout(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
db := a.db.WithContext(ctx)
scope := LogoutGlobal
if r.URL.Query() != nil {
switch r.URL.Query().Get("scope") {
case "", "global":
scope = LogoutGlobal
case "local":
scope = LogoutLocal
case "others":
scope = LogoutOthers
default:
return badRequestError(ErrorCodeValidationFailed, fmt.Sprintf("Unsupported logout scope %q", r.URL.Query().Get("scope")))
}
}
s := getSession(ctx)
u := getUser(ctx)
err := db.Transaction(func(tx *storage.Connection) error {
if terr := models.NewAuditLogEntry(r, tx, u, models.LogoutAction, "", nil); terr != nil {
return terr
}
if s == nil {
logrus.Infof("user has an empty session_id claim: %s", u.ID)
} else {
//exhaustive:ignore Default case is handled below.
switch scope {
case LogoutLocal:
return models.LogoutSession(tx, s.ID)
case LogoutOthers:
return models.LogoutAllExceptMe(tx, s.ID, u.ID)
}
}
// default mode, log out everywhere
return models.Logout(tx, u.ID)
})
if err != nil {
return internalServerError("Error logging out user").WithInternalError(err)
}
w.WriteHeader(http.StatusNoContent)
return nil
}

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