Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Build Docker Image

on:
workflow_dispatch:
push:
branches:
- "**"
paths:
- "**"

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 }}
2 changes: 1 addition & 1 deletion Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion hack/migrate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions internal/api/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
109 changes: 109 additions & 0 deletions internal/api/provider/seznam.go
Original file line number Diff line number Diff line change
@@ -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 = "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 true
}

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
}
83 changes: 83 additions & 0 deletions internal/api/provider/seznam_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
1 change: 1 addition & 0 deletions internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
Loading