From 3e2d0811cf3b53fb4bf81c4b163573423b2f210e Mon Sep 17 00:00:00 2001 From: Martin Georgiu Date: Wed, 7 Jan 2026 16:31:34 +0100 Subject: [PATCH 1/5] add seznam --- .github/workflows/docker-build.yml | 46 +++++++++++ Dockerfile.dev | 2 +- Makefile | 2 +- docker-compose-dev.yml | 2 + hack/migrate.sh | 3 +- internal/api/external.go | 3 + internal/api/provider/seznam.go | 109 +++++++++++++++++++++++++++ internal/api/provider/seznam_test.go | 83 ++++++++++++++++++++ internal/conf/configuration.go | 1 + 9 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/docker-build.yml create mode 100644 internal/api/provider/seznam.go create mode 100644 internal/api/provider/seznam_test.go diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000000..6260e75f47 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,46 @@ +name: Build Docker Image + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "**" + - "!.github/**" + - ".github/workflows/docker-build.yml" + +permissions: + actions: read + checks: write + contents: write + deployments: write + packages: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to GHCR + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute short sha + id: vars + run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Build and push image + uses: docker/build-push-action@v4 + with: + context: . + file: ./Dockerfile + push: true + tags: | + ghcr.io/${{ github.repository }}/supabase-auth:latest + ghcr.io/${{ github.repository }}/supabase-auth:${{ steps.vars.outputs.sha_short }} diff --git a/Dockerfile.dev b/Dockerfile.dev index 99a8c0d5cb..9405f38388 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -3,7 +3,7 @@ ENV GO111MODULE=on ENV CGO_ENABLED=0 ENV GOOS=linux -RUN apk add --no-cache make git bash +RUN apk add --no-cache make git bash build-base WORKDIR /go/src/github.com/supabase/auth diff --git a/Makefile b/Makefile index 14d41aa1c5..6401f9ec4a 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ 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 + POSTGRES_HOST=$${POSTGRES_HOST:-localhost} go test $(CHECK_FILES) -coverprofile=coverage.out -coverpkg ./... -p 1 -race -v -count=1 ./hack/coverage.sh vet: # Vet the code diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index d5c6173c21..7f895f6f15 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -10,6 +10,8 @@ services: - '9100:9100' environment: - GOTRUE_DB_MIGRATIONS_PATH=/go/src/github.com/supabase/auth/migrations + - POSTGRES_HOST=postgres + - CGO_ENABLED=1 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" diff --git a/hack/migrate.sh b/hack/migrate.sh index 2d1f0e5e84..8fee7839d2 100755 --- a/hack/migrate.sh +++ b/hack/migrate.sh @@ -6,7 +6,8 @@ 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" +HOST=${POSTGRES_HOST:-localhost} +export GOTRUE_DB_DATABASE_URL="postgres://supabase_auth_admin:root@$HOST:5432/$DB_ENV" export GOTRUE_DB_MIGRATIONS_PATH=$DIR/../migrations go run main.go migrate -c $DIR/test.env diff --git a/internal/api/external.go b/internal/api/external.go index 8392797d59..37b389e8ca 100644 --- a/internal/api/external.go +++ b/internal/api/external.go @@ -674,6 +674,9 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide case "spotify": pConfig = config.External.Spotify p, err = provider.NewSpotifyProvider(pConfig, scopes) + case "seznam": + pConfig = config.External.Seznam + p, err = provider.NewSeznamProvider(pConfig, scopes) case "slack": pConfig = config.External.Slack p, err = provider.NewSlackProvider(pConfig, scopes) diff --git a/internal/api/provider/seznam.go b/internal/api/provider/seznam.go new file mode 100644 index 0000000000..03df02d757 --- /dev/null +++ b/internal/api/provider/seznam.go @@ -0,0 +1,109 @@ +package provider + +import ( + "context" + "errors" + "strings" + + "github.com/supabase/auth/internal/conf" + "golang.org/x/oauth2" +) + +// Seznam provider constants +const ( + defaultSeznamAuthURL = "https://login.szn.cz/api/v1/oauth/auth" + defaultSeznamTokenURL = "https://login.szn.cz/api/v1/oauth/token" + defaultSeznamUserURL = "https://login.szn.cz/api/v1/user" +) + +type seznamProvider struct { + *oauth2.Config + APIURL string +} + +type seznamUser struct { + ID string `json:"oauth_user_id"` + Email string `json:"email"` + Name string `json:"firstname"` + FamilyName string `json:"lastname"` + AvatarURL string `json:"avatar_url"` + EmailVerified bool `json:"email_verified"` // This might need to be checked against real response, docs claim 'identity' scope provides email +} + +// NewSeznamProvider creates a Seznam OAuth2 identity provider. +func NewSeznamProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { + if err := ext.ValidateOAuth(); err != nil { + return nil, err + } + + oauthScopes := []string{ + "identity", + } + + if scopes != "" { + oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...) + } + + apiPath := chooseHost(ext.URL, defaultSeznamUserURL) + + return &seznamProvider{ + Config: &oauth2.Config{ + ClientID: ext.ClientID[0], + ClientSecret: ext.Secret, + Endpoint: oauth2.Endpoint{ + AuthURL: defaultSeznamAuthURL, + TokenURL: defaultSeznamTokenURL, + }, + RedirectURL: ext.RedirectURI, + Scopes: oauthScopes, + }, + APIURL: apiPath, + }, nil +} + +func (g seznamProvider) GetOAuthToken(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + return g.Exchange(ctx, code, opts...) +} + +func (g seznamProvider) RequiresPKCE() bool { + return false +} + +func (g seznamProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { + var u seznamUser + if err := makeRequest(ctx, tok, g.Config, g.APIURL, &u); err != nil { + return nil, err + } + + if u.ID == "" { + return nil, errors.New("user id was not returned from Seznam API") + } + + var data UserProvidedData + + if u.Email != "" { + data.Emails = append(data.Emails, Email{ + Email: u.Email, + Verified: true, // Seznam verifies emails + Primary: true, + }) + } + + data.Metadata = &Claims{ + Issuer: g.APIURL, + Subject: u.ID, + Name: strings.TrimSpace(u.Name + " " + u.FamilyName), + GivenName: u.Name, + FamilyName: u.FamilyName, + Picture: u.AvatarURL, + Email: u.Email, + EmailVerified: true, // Seznam verifies emails + + // To be deprecated + AvatarURL: u.AvatarURL, + FullName: strings.TrimSpace(u.Name + " " + u.FamilyName), + ProviderId: u.ID, + } + + return &data, nil +} diff --git a/internal/api/provider/seznam_test.go b/internal/api/provider/seznam_test.go new file mode 100644 index 0000000000..4a0351f26d --- /dev/null +++ b/internal/api/provider/seznam_test.go @@ -0,0 +1,83 @@ +package provider + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/supabase/auth/internal/conf" + "golang.org/x/oauth2" +) + +func TestSeznam(t *testing.T) { + t.Run("GetUserData", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "Bearer token", r.Header.Get("Authorization")) + w.Write([]byte(`{"oauth_user_id":"test_id","email":"test@example.com","firstname":"Test","lastname":"User","avatar_url":"http://example.com/avatar"}`)) + })) + defer server.Close() + + p := seznamProvider{ + Config: &oauth2.Config{}, + APIURL: server.URL, + } + + data, err := p.GetUserData(context.Background(), &oauth2.Token{ + AccessToken: "token", + }) + assert.NoError(t, err) + assert.Equal(t, "test@example.com", data.Emails[0].Email) + assert.Equal(t, "test_id", data.Metadata.Subject) + assert.Equal(t, "Test User", data.Metadata.Name) + assert.Equal(t, "http://example.com/avatar", data.Metadata.Picture) + }) + + t.Run("GetUserData Error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + p := seznamProvider{ + Config: &oauth2.Config{}, + APIURL: server.URL, + } + + _, err := p.GetUserData(context.Background(), &oauth2.Token{ + AccessToken: "token", + }) + assert.Error(t, err) + }) + + t.Run("GetUserData Missing ID", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"email":"test@example.com"}`)) + })) + defer server.Close() + + p := seznamProvider{ + Config: &oauth2.Config{}, + APIURL: server.URL, + } + + _, err := p.GetUserData(context.Background(), &oauth2.Token{ + AccessToken: "token", + }) + assert.Error(t, err) + assert.Equal(t, errors.New("user id was not returned from Seznam API"), err) + }) + + t.Run("NewSeznamProvider", func(t *testing.T) { + p, err := NewSeznamProvider(conf.OAuthProviderConfiguration{ + ClientID: []string{"client_id"}, + Secret: "secret", + RedirectURI: "http://localhost/callback", + URL: "http://example.com", + }, "") + assert.NoError(t, err) + assert.NotNil(t, p) + }) +} diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index 3e397be69c..a70144432c 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -430,6 +430,7 @@ type ProviderConfiguration struct { Keycloak OAuthProviderConfiguration `json:"keycloak"` Linkedin OAuthProviderConfiguration `json:"linkedin"` LinkedinOIDC OAuthProviderConfiguration `json:"linkedin_oidc" envconfig:"LINKEDIN_OIDC"` + Seznam OAuthProviderConfiguration `json:"seznam"` Spotify OAuthProviderConfiguration `json:"spotify"` Slack OAuthProviderConfiguration `json:"slack"` SlackOIDC OAuthProviderConfiguration `json:"slack_oidc" envconfig:"SLACK_OIDC"` From 59447915658745a1ba5de8be0f098a33e9708f6f Mon Sep 17 00:00:00 2001 From: Martin Georgiu Date: Wed, 7 Jan 2026 16:46:13 +0100 Subject: [PATCH 2/5] add pkce --- internal/api/provider/seznam.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/provider/seznam.go b/internal/api/provider/seznam.go index 03df02d757..971ab48db7 100644 --- a/internal/api/provider/seznam.go +++ b/internal/api/provider/seznam.go @@ -66,7 +66,7 @@ func (g seznamProvider) GetOAuthToken(ctx context.Context, code string, opts ... } func (g seznamProvider) RequiresPKCE() bool { - return false + return true } func (g seznamProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { From 33e880a04f6ecc3d345a3ef37c63c60ba06e0e84 Mon Sep 17 00:00:00 2001 From: Martin Georgiu Date: Wed, 7 Jan 2026 16:48:40 +0100 Subject: [PATCH 3/5] add --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 6260e75f47..a0e732cf43 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: branches: - - main + - "**" paths: - "**" - "!.github/**" From 6ceb0227d4a0a921acfda9b7008d606af87f4cb8 Mon Sep 17 00:00:00 2001 From: Martin Georgiu Date: Wed, 7 Jan 2026 16:48:53 +0100 Subject: [PATCH 4/5] add --- .github/workflows/docker-build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index a0e732cf43..f3d245c6f3 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -7,8 +7,6 @@ on: - "**" paths: - "**" - - "!.github/**" - - ".github/workflows/docker-build.yml" permissions: actions: read From 9bc4fbf3ed7bcdcec5e192dc830f32b82590ebf9 Mon Sep 17 00:00:00 2001 From: Martin Georgiu Date: Thu, 8 Jan 2026 10:54:17 +0100 Subject: [PATCH 5/5] fix url --- internal/api/provider/seznam.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/provider/seznam.go b/internal/api/provider/seznam.go index 971ab48db7..d530912b03 100644 --- a/internal/api/provider/seznam.go +++ b/internal/api/provider/seznam.go @@ -13,7 +13,7 @@ import ( const ( defaultSeznamAuthURL = "https://login.szn.cz/api/v1/oauth/auth" defaultSeznamTokenURL = "https://login.szn.cz/api/v1/oauth/token" - defaultSeznamUserURL = "https://login.szn.cz/api/v1/user" + defaultSeznamUserURL = "login.szn.cz/api/v1/user" ) type seznamProvider struct {