Merge branch 'main' into publish-image

This commit is contained in:
Elio Bischof
2025-04-02 12:29:38 +02:00
committed by GitHub
34 changed files with 1645 additions and 283 deletions

View File

@@ -2,9 +2,9 @@ name: Quality
on:
pull_request:
schedule:
# schedule:
# Every morning at 6:00 AM CET
- cron: '0 4 * * *'
# - cron: '0 4 * * *'
workflow_dispatch:
inputs:
target-env:
@@ -110,6 +110,14 @@ jobs:
run: pnpm build
if: ${{ startsWith(matrix.command, 'test:acceptance') }}
- name: Run SAML SP
run: ZITADEL_DEV_UID=root pnpm run-samlsp
if: ${{ matrix.command == 'test:acceptance' }}
- name: Run OIDC RP
run: ZITADEL_DEV_UID=root pnpm run-oidcrp
if: ${{ matrix.command == 'test:acceptance' }}
- name: Check
id: check
run: pnpm ${{ contains(matrix.command, 'test:acceptance') && 'test:acceptance' || matrix.command }}

View File

@@ -47,11 +47,8 @@ export ZITADEL_DEV_UID="$(id -u)"
# Pull images
docker compose --file ./acceptance/docker-compose.yaml pull
# Run ZITADEL and configure ./apps/login/.env.local
docker compose --file ./acceptance/docker-compose.yaml run setup
# Configure your shell to use the environment variables written to ./apps/login/.env.acceptance
export $(cat ./apps/login/.env.acceptance | xargs)
# Run ZITADEL with local notification sink and configure ./apps/login/.env.local
pnpm run-sink
```
### Developing Against Your ZITADEL Cloud Instance
@@ -79,6 +76,22 @@ pnpm dev
The application is now available at `http://localhost:3000`
### Adding applications and IDPs
```sh
# OPTIONAL Run SAML SP
pnpm run-samlsp
# OPTIONAL Run OIDC RP
pnpm run-oidcrp
# OPTIONAL Run SAML IDP
pnpm run-samlidp
# OPTIONAL Run OIDC OP
pnpm run-oidcop
```
### Testing
You can execute the following commands `pnpm test` for a single test run or `pnpm test:watch` in the following directories:

View File

@@ -1,4 +1,4 @@
FROM golang:1.19-alpine
FROM golang:1.24-alpine
RUN apk add curl jq
COPY setup.sh /setup.sh
RUN chmod +x /setup.sh

View File

@@ -1,7 +1,7 @@
services:
zitadel:
user: "${ZITADEL_DEV_UID}"
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v2.67.2}"
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:02617cf17fdde849378c1a6b5254bbfb2745b164}"
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml'
ports:
- "8080:8080"
@@ -11,6 +11,8 @@ services:
depends_on:
db:
condition: "service_healthy"
extra_hosts:
- "localhost:host-gateway"
db:
restart: "always"
@@ -57,7 +59,7 @@ services:
condition: "service_completed_successfully"
sink:
image: golang:1.19-alpine
image: golang:1.24-alpine
container_name: sink
command: go run /sink/main.go -port '3333' -email '/email' -sms '/sms' -notification '/notification'
ports:

View File

@@ -0,0 +1,20 @@
services:
oidcop:
image: golang:1.24-alpine
container_name: oidcop
command: go run main.go
environment:
API_URL: 'http://localhost:8080'
API_DOMAIN: 'localhost:8080'
PAT_FILE: '/pat/zitadel-admin-sa.pat'
SCHEMA: 'http'
HOST: 'localhost'
PORT: "8004"
working_dir: /oidc
ports:
- 8004:8004
volumes:
- "../../pat:/pat"
- "./:/oidc"
extra_hosts:
- "localhost:host-gateway"

View File

@@ -0,0 +1,28 @@
module github.com/zitadel/typescript/acceptance/idp/oidc
go 1.24.1
require github.com/zitadel/oidc/v3 v3.37.0
require (
github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/muhlemmer/gu v0.3.1 // indirect
github.com/muhlemmer/httpforwarded v0.1.0 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/zitadel/logging v0.6.2 // indirect
github.com/zitadel/schema v1.3.1 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
)

View File

@@ -0,0 +1,71 @@
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
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/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/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/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
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/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU=
github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4=
github.com/zitadel/oidc/v3 v3.37.0 h1:nYATWlnP7f18XiAbw6upUruBaqfB1kUrXrSTf1EYGO8=
github.com/zitadel/oidc/v3 v3.37.0/go.mod h1:/xDan4OUQhguJ4Ur73OOJrtugvR164OMnidXP9xfVNw=
github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU=
github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

186
acceptance/idp/oidc/main.go Normal file
View File

@@ -0,0 +1,186 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/zitadel/oidc/v3/example/server/exampleop"
"github.com/zitadel/oidc/v3/example/server/storage"
)
func main() {
apiURL := os.Getenv("API_URL")
pat := readPAT(os.Getenv("PAT_FILE"))
domain := os.Getenv("API_DOMAIN")
schema := os.Getenv("SCHEMA")
host := os.Getenv("HOST")
port := os.Getenv("PORT")
logger := slog.New(
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
}),
)
issuer := fmt.Sprintf("%s://%s:%s/", schema, host, port)
redirectURI := fmt.Sprintf("%s/idps/callback", apiURL)
clientID := "web"
clientSecret := "secret"
storage.RegisterClients(
storage.WebClient(clientID, clientSecret, redirectURI),
)
storage := storage.NewStorage(storage.NewUserStore(issuer))
router := exampleop.SetupServer(issuer, storage, logger, false)
server := &http.Server{
Addr: ":" + port,
Handler: router,
}
go func() {
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP server error: %v", err)
}
log.Println("Stopped serving new connections.")
}()
createZitadelResources(apiURL, pat, domain, issuer, clientID, clientSecret)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
}
func readPAT(path string) string {
f, err := os.Open(path)
if err != nil {
panic(err)
}
pat, err := io.ReadAll(f)
if err != nil {
panic(err)
}
return strings.Trim(string(pat), "\n")
}
func createZitadelResources(apiURL, pat, domain, issuer, clientID, clientSecret string) error {
idpID, err := CreateIDP(apiURL, pat, domain, issuer, clientID, clientSecret)
if err != nil {
return err
}
return ActivateIDP(apiURL, pat, domain, idpID)
}
type createIDP struct {
Name string `json:"name"`
Issuer string `json:"issuer"`
ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
Scopes []string `json:"scopes"`
ProviderOptions providerOptions `json:"providerOptions"`
IsIdTokenMapping bool `json:"isIdTokenMapping"`
UsePkce bool `json:"usePkce"`
}
type providerOptions struct {
IsLinkingAllowed bool `json:"isLinkingAllowed"`
IsCreationAllowed bool `json:"isCreationAllowed"`
IsAutoCreation bool `json:"isAutoCreation"`
IsAutoUpdate bool `json:"isAutoUpdate"`
AutoLinking string `json:"autoLinking"`
}
type idp struct {
ID string `json:"id"`
}
func CreateIDP(apiURL, pat, domain string, issuer, clientID, clientSecret string) (string, error) {
createIDP := &createIDP{
Name: "OIDC",
Issuer: issuer,
ClientId: clientID,
ClientSecret: clientSecret,
Scopes: []string{"openid", "profile", "email"},
ProviderOptions: providerOptions{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
AutoLinking: "AUTO_LINKING_OPTION_USERNAME",
},
IsIdTokenMapping: false,
UsePkce: false,
}
resp, err := doRequestWithHeaders(apiURL+"/admin/v1/idps/generic_oidc", pat, domain, createIDP)
if err != nil {
return "", err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
defer resp.Body.Close()
idp := new(idp)
if err := json.Unmarshal(data, idp); err != nil {
return "", err
}
return idp.ID, nil
}
type activateIDP struct {
IdpId string `json:"idpId"`
}
func ActivateIDP(apiURL, pat, domain string, idpID string) error {
activateIDP := &activateIDP{
IdpId: idpID,
}
_, err := doRequestWithHeaders(apiURL+"/admin/v1/policies/login/idps", pat, domain, activateIDP)
return err
}
func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data)))
if err != nil {
return nil, err
}
values := http.Header{}
values.Add("Authorization", "Bearer "+pat)
values.Add("x-forwarded-host", domain)
values.Add("Content-Type", "application/json")
req.Header = values
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}

View File

@@ -0,0 +1,20 @@
services:
samlidp:
image: golang:1.24-alpine
container_name: samlidp
command: go run main.go
environment:
API_URL: 'http://localhost:8080'
API_DOMAIN: 'localhost:8080'
PAT_FILE: '/pat/zitadel-admin-sa.pat'
SCHEMA: 'http'
HOST: 'localhost'
PORT: "8003"
working_dir: /saml
ports:
- 8003:8003
volumes:
- "../../pat:/pat"
- "./:/saml"
extra_hosts:
- "localhost:host-gateway"

View File

@@ -0,0 +1,16 @@
module github.com/zitadel/typescript/acceptance/idp/saml
go 1.24.1
require (
github.com/crewjam/saml v0.4.14
github.com/mattermost/xml-roundtrip-validator v0.1.0
github.com/zenazn/goji v1.0.1
golang.org/x/crypto v0.36.0
)
require (
github.com/beevik/etree v1.1.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/russellhaering/goxmldsig v1.3.0 // indirect
)

View File

@@ -0,0 +1,49 @@
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/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/russellhaering/goxmldsig v1.3.0 h1:DllIWUgMy0cRUMfGiASiYEa35nsieyD3cigIwLonTPM=
github.com/russellhaering/goxmldsig v1.3.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8=
github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
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-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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=

328
acceptance/idp/saml/main.go Normal file
View File

@@ -0,0 +1,328 @@
package main
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"encoding/xml"
"errors"
"io"
"log"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"github.com/crewjam/saml"
"github.com/crewjam/saml/logger"
"github.com/crewjam/saml/samlidp"
xrv "github.com/mattermost/xml-roundtrip-validator"
"github.com/zenazn/goji"
"github.com/zenazn/goji/bind"
"github.com/zenazn/goji/web"
"golang.org/x/crypto/bcrypt"
)
var key = func() crypto.PrivateKey {
b, _ := pem.Decode([]byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA0OhbMuizgtbFOfwbK7aURuXhZx6VRuAs3nNibiuifwCGz6u9
yy7bOR0P+zqN0YkjxaokqFgra7rXKCdeABmoLqCC0U+cGmLNwPOOA0PaD5q5xKhQ
4Me3rt/R9C4Ca6k3/OnkxnKwnogcsmdgs2l8liT3qVHP04Oc7Uymq2v09bGb6nPu
fOrkXS9F6mSClxHG/q59AGOWsXK1xzIRV1eu8W2SNdyeFVU1JHiQe444xLoPul5t
InWasKayFsPlJfWNc8EoU8COjNhfo/GovFTHVjh9oUR/gwEFVwifIHihRE0Hazn2
EQSLaOr2LM0TsRsQroFjmwSGgI+X2bfbMTqWOQIDAQABAoIBAFWZwDTeESBdrLcT
zHZe++cJLxE4AObn2LrWANEv5AeySYsyzjRBYObIN9IzrgTb8uJ900N/zVr5VkxH
xUa5PKbOcowd2NMfBTw5EEnaNbILLm+coHdanrNzVu59I9TFpAFoPavrNt/e2hNo
NMGPSdOkFi81LLl4xoadz/WR6O/7N2famM+0u7C2uBe+TrVwHyuqboYoidJDhO8M
w4WlY9QgAUhkPyzZqrl+VfF1aDTGVf4LJgaVevfFCas8Ws6DQX5q4QdIoV6/0vXi
B1M+aTnWjHuiIzjBMWhcYW2+I5zfwNWRXaxdlrYXRukGSdnyO+DH/FhHePJgmlkj
NInADDkCgYEA6MEQFOFSCc/ELXYWgStsrtIlJUcsLdLBsy1ocyQa2lkVUw58TouW
RciE6TjW9rp31pfQUnO2l6zOUC6LT9Jvlb9PSsyW+rvjtKB5PjJI6W0hjX41wEO6
fshFELMJd9W+Ezao2AsP2hZJ8McCF8no9e00+G4xTAyxHsNI2AFTCQcCgYEA5cWZ
JwNb4t7YeEajPt9xuYNUOQpjvQn1aGOV7KcwTx5ELP/Hzi723BxHs7GSdrLkkDmi
Gpb+mfL4wxCt0fK0i8GFQsRn5eusyq9hLqP/bmjpHoXe/1uajFbE1fZQR+2LX05N
3ATlKaH2hdfCJedFa4wf43+cl6Yhp6ZA0Yet1r8CgYEAwiu1j8W9G+RRA5/8/DtO
yrUTOfsbFws4fpLGDTA0mq0whf6Soy/96C90+d9qLaC3srUpnG9eB0CpSOjbXXbv
kdxseLkexwOR3bD2FHX8r4dUM2bzznZyEaxfOaQypN8SV5ME3l60Fbr8ajqLO288
wlTmGM5Mn+YCqOg/T7wjGmcCgYBpzNfdl/VafOROVbBbhgXWtzsz3K3aYNiIjbp+
MunStIwN8GUvcn6nEbqOaoiXcX4/TtpuxfJMLw4OvAJdtxUdeSmEee2heCijV6g3
ErrOOy6EqH3rNWHvlxChuP50cFQJuYOueO6QggyCyruSOnDDuc0BM0SGq6+5g5s7
H++S/wKBgQDIkqBtFr9UEf8d6JpkxS0RXDlhSMjkXmkQeKGFzdoJcYVFIwq8jTNB
nJrVIGs3GcBkqGic+i7rTO1YPkquv4dUuiIn+vKZVoO6b54f+oPBXd4S0BnuEqFE
rdKNuCZhiaE2XD9L/O9KP1fh5bfEcKwazQ23EvpJHBMm8BGC+/YZNw==
-----END RSA PRIVATE KEY-----`))
k, _ := x509.ParsePKCS1PrivateKey(b.Bytes)
return k
}()
var cert = func() *x509.Certificate {
b, _ := pem.Decode([]byte(`-----BEGIN CERTIFICATE-----
MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV
BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5
NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8A
hs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+a
ucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWx
m+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6
D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURN
B2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0O
BBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56
zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5
pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uv
NONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEf
y/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL
/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsb
GFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTL
UzreO96WzlBBMtY=
-----END CERTIFICATE-----`))
c, _ := x509.ParseCertificate(b.Bytes)
return c
}()
// Example from https://github.com/crewjam/saml/blob/main/example/idp/idp.go
func main() {
apiURL := os.Getenv("API_URL")
pat := readPAT(os.Getenv("PAT_FILE"))
domain := os.Getenv("API_DOMAIN")
schema := os.Getenv("SCHEMA")
host := os.Getenv("HOST")
port := os.Getenv("PORT")
baseURL, err := url.Parse(schema + "://" + host + ":" + port)
if err != nil {
panic(err)
}
idpServer, err := samlidp.New(samlidp.Options{
URL: *baseURL,
Logger: logger.DefaultLogger,
Key: key,
Certificate: cert,
Store: &samlidp.MemoryStore{},
})
if err != nil {
panic(err)
}
metadata, err := xml.MarshalIndent(idpServer.IDP.Metadata(), "", " ")
if err != nil {
panic(err)
}
idpID, err := createZitadelResources(apiURL, pat, domain, metadata)
if err != nil {
panic(err)
}
lis := bind.Socket(":" + baseURL.Port())
goji.Handle("/*", idpServer)
go func() {
goji.ServeListener(lis)
}()
addService(idpServer, apiURL+"/idps/"+idpID+"/saml/metadata")
addUsers(idpServer)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
if err := lis.Close(); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
}
func readPAT(path string) string {
f, err := os.Open(path)
if err != nil {
panic(err)
}
pat, err := io.ReadAll(f)
if err != nil {
panic(err)
}
return strings.Trim(string(pat), "\n")
}
func addService(idpServer *samlidp.Server, spURLStr string) {
metadataResp, err := http.Get(spURLStr)
if err != nil {
panic(err)
}
defer metadataResp.Body.Close()
idpServer.HandlePutService(
web.C{URLParams: map[string]string{"id": spURLStr}},
httptest.NewRecorder(),
httptest.NewRequest(http.MethodPost, spURLStr, metadataResp.Body),
)
}
func getSPMetadata(r io.Reader) (spMetadata *saml.EntityDescriptor, err error) {
var data []byte
if data, err = io.ReadAll(r); err != nil {
return nil, err
}
spMetadata = &saml.EntityDescriptor{}
if err := xrv.Validate(bytes.NewBuffer(data)); err != nil {
return nil, err
}
if err := xml.Unmarshal(data, &spMetadata); err != nil {
if err.Error() == "expected element type <EntityDescriptor> but have <EntitiesDescriptor>" {
entities := &saml.EntitiesDescriptor{}
if err := xml.Unmarshal(data, &entities); err != nil {
return nil, err
}
for _, e := range entities.EntityDescriptors {
if len(e.SPSSODescriptors) > 0 {
return &e, nil
}
}
// there were no SPSSODescriptors in the response
return nil, errors.New("metadata contained no service provider metadata")
}
return nil, err
}
return spMetadata, nil
}
func addUsers(idpServer *samlidp.Server) {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("hunter2"), bcrypt.DefaultCost)
err := idpServer.Store.Put("/users/alice", samlidp.User{Name: "alice",
HashedPassword: hashedPassword,
Groups: []string{"Administrators", "Users"},
Email: "alice@example.com",
CommonName: "Alice Smith",
Surname: "Smith",
GivenName: "Alice",
})
if err != nil {
panic(err)
}
err = idpServer.Store.Put("/users/bob", samlidp.User{
Name: "bob",
HashedPassword: hashedPassword,
Groups: []string{"Users"},
Email: "bob@example.com",
CommonName: "Bob Smith",
Surname: "Smith",
GivenName: "Bob",
})
if err != nil {
panic(err)
}
}
func createZitadelResources(apiURL, pat, domain string, metadata []byte) (string, error) {
idpID, err := CreateIDP(apiURL, pat, domain, metadata)
if err != nil {
return "", err
}
return idpID, ActivateIDP(apiURL, pat, domain, idpID)
}
type createIDP struct {
Name string `json:"name"`
MetadataXml string `json:"metadataXml"`
Binding string `json:"binding"`
WithSignedRequest bool `json:"withSignedRequest"`
ProviderOptions providerOptions `json:"providerOptions"`
NameIdFormat string `json:"nameIdFormat"`
}
type providerOptions struct {
IsLinkingAllowed bool `json:"isLinkingAllowed"`
IsCreationAllowed bool `json:"isCreationAllowed"`
IsAutoCreation bool `json:"isAutoCreation"`
IsAutoUpdate bool `json:"isAutoUpdate"`
AutoLinking string `json:"autoLinking"`
}
type idp struct {
ID string `json:"id"`
}
func CreateIDP(apiURL, pat, domain string, idpMetadata []byte) (string, error) {
encoded := make([]byte, base64.URLEncoding.EncodedLen(len(idpMetadata)))
base64.URLEncoding.Encode(encoded, idpMetadata)
createIDP := &createIDP{
Name: "CREWJAM",
MetadataXml: string(encoded),
Binding: "SAML_BINDING_REDIRECT",
WithSignedRequest: false,
ProviderOptions: providerOptions{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
AutoLinking: "AUTO_LINKING_OPTION_USERNAME",
},
NameIdFormat: "SAML_NAME_ID_FORMAT_PERSISTENT",
}
resp, err := doRequestWithHeaders(apiURL+"/admin/v1/idps/saml", pat, domain, createIDP)
if err != nil {
return "", err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
defer resp.Body.Close()
idp := new(idp)
if err := json.Unmarshal(data, idp); err != nil {
return "", err
}
return idp.ID, nil
}
type activateIDP struct {
IdpId string `json:"idpId"`
}
func ActivateIDP(apiURL, pat, domain string, idpID string) error {
activateIDP := &activateIDP{
IdpId: idpID,
}
_, err := doRequestWithHeaders(apiURL+"/admin/v1/policies/login/idps", pat, domain, activateIDP)
return err
}
func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data)))
if err != nil {
return nil, err
}
values := http.Header{}
values.Add("Authorization", "Bearer "+pat)
values.Add("x-forwarded-host", domain)
values.Add("Content-Type", "application/json")
req.Header = values
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}

View File

@@ -0,0 +1,22 @@
services:
oidcrp:
image: golang:1.24-alpine
container_name: oidcrp
command: go run main.go
environment:
API_URL: 'http://localhost:8080'
API_DOMAIN: 'localhost:8080'
PAT_FILE: '/pat/zitadel-admin-sa.pat'
LOGIN_URL: 'http://localhost:3000'
ISSUER: 'http://localhost:3000'
HOST: 'http://localhost'
PORT: '8000'
SCOPES: 'openid profile email'
working_dir: /oidc
ports:
- 8000:8000
volumes:
- "../pat:/pat"
- "./:/oidc"
extra_hosts:
- "localhost:host-gateway"

26
acceptance/oidc/go.mod Normal file
View File

@@ -0,0 +1,26 @@
module github.com/zitadel/typescript/acceptance/oidc
go 1.24.1
require (
github.com/google/uuid v1.6.0
github.com/sirupsen/logrus v1.9.3
github.com/zitadel/logging v0.6.1
github.com/zitadel/oidc/v3 v3.36.1
)
require (
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/muhlemmer/gu v0.3.1 // indirect
github.com/zitadel/schema v1.3.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
)

67
acceptance/oidc/go.sum Normal file
View File

@@ -0,0 +1,67 @@
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
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/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/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/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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA=
github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
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/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y=
github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow=
github.com/zitadel/oidc/v3 v3.36.1 h1:1AT1NqKKEqAwx4GmKJZ9fYkWH2WIn/VKMfQ46nBtRf0=
github.com/zitadel/oidc/v3 v3.36.1/go.mod h1:dApGZLvWZTHRuxmcbQlW5d2XVjVYR3vGOdq536igmTs=
github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=

313
acceptance/oidc/main.go Normal file
View File

@@ -0,0 +1,313 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"strings"
"sync/atomic"
"syscall"
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
var (
callbackPath = "/auth/callback"
key = []byte("test1234test1234")
)
func main() {
apiURL := os.Getenv("API_URL")
pat := readPAT(os.Getenv("PAT_FILE"))
domain := os.Getenv("API_DOMAIN")
loginURL := os.Getenv("LOGIN_URL")
issuer := os.Getenv("ISSUER")
host := os.Getenv("HOST")
port := os.Getenv("PORT")
scopeList := strings.Split(os.Getenv("SCOPES"), " ")
redirectURI := fmt.Sprintf("%v:%v%v", host, port, callbackPath)
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
clientID, clientSecret, err := createZitadelResources(apiURL, pat, domain, redirectURI, loginURL)
if err != nil {
panic(err)
}
logger := slog.New(
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
}),
)
client := &http.Client{
Timeout: time.Minute,
}
// enable outgoing request logging
logging.EnableHTTPClient(client,
logging.WithClientGroup("client"),
)
options := []rp.Option{
rp.WithCookieHandler(cookieHandler),
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
rp.WithHTTPClient(client),
rp.WithLogger(logger),
rp.WithSigningAlgsFromDiscovery(),
}
if clientSecret == "" {
options = append(options, rp.WithPKCE(cookieHandler))
}
// One can add a logger to the context,
// pre-defining log attributes as required.
ctx := logging.ToContext(context.TODO(), logger)
provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, redirectURI, scopeList, options...)
if err != nil {
logrus.Fatalf("error creating provider %s", err.Error())
}
// generate some state (representing the state of the user in your application,
// e.g. the page where he was before sending him to login
state := func() string {
return uuid.New().String()
}
urlOptions := []rp.URLParamOpt{
rp.WithPromptURLParam("Welcome back!"),
}
// register the AuthURLHandler at your preferred path.
// the AuthURLHandler creates the auth request and redirects the user to the auth server.
// including state handling with secure cookie and the possibility to use PKCE.
// Prompts can optionally be set to inform the server of
// any messages that need to be prompted back to the user.
http.Handle("/login", rp.AuthURLHandler(
state,
provider,
urlOptions...,
))
// for demonstration purposes the returned userinfo response is written as JSON object onto response
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
fmt.Println("access token", tokens.AccessToken)
fmt.Println("refresh token", tokens.RefreshToken)
fmt.Println("id token", tokens.IDToken)
data, err := json.Marshal(info)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("content-type", "application/json")
w.Write(data)
}
// register the CodeExchangeHandler at the callbackPath
// the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function
// with the returned tokens from the token endpoint
// in this example the callback function itself is wrapped by the UserinfoCallback which
// will call the Userinfo endpoint, check the sub and pass the info into the callback function
http.Handle(callbackPath, rp.CodeExchangeHandler(rp.UserinfoCallback(marshalUserinfo), provider))
// if you would use the callback without calling the userinfo endpoint, simply switch the callback handler for:
//
// http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider))
// simple counter for request IDs
var counter atomic.Int64
// enable incomming request logging
mw := logging.Middleware(
logging.WithLogger(logger),
logging.WithGroup("server"),
logging.WithIDFunc(func() slog.Attr {
return slog.Int64("id", counter.Add(1))
}),
)
server := &http.Server{
Addr: ":" + port,
Handler: mw(http.DefaultServeMux),
}
go func() {
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP server error: %v", err)
}
log.Println("Stopped serving new connections.")
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
}
func readPAT(path string) string {
f, err := os.Open(path)
if err != nil {
panic(err)
}
pat, err := io.ReadAll(f)
if err != nil {
panic(err)
}
return strings.Trim(string(pat), "\n")
}
func createZitadelResources(apiURL, pat, domain, redirectURI, loginURL string) (string, string, error) {
projectID, err := CreateProject(apiURL, pat, domain)
if err != nil {
return "", "", err
}
return CreateApp(apiURL, pat, domain, projectID, redirectURI, loginURL)
}
type project struct {
ID string `json:"id"`
}
type createProject struct {
Name string `json:"name"`
ProjectRoleAssertion bool `json:"projectRoleAssertion"`
ProjectRoleCheck bool `json:"projectRoleCheck"`
HasProjectCheck bool `json:"hasProjectCheck"`
PrivateLabelingSetting string `json:"privateLabelingSetting"`
}
func CreateProject(apiURL, pat, domain string) (string, error) {
createProject := &createProject{
Name: "OIDC",
ProjectRoleAssertion: false,
ProjectRoleCheck: false,
HasProjectCheck: false,
PrivateLabelingSetting: "PRIVATE_LABELING_SETTING_UNSPECIFIED",
}
resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects", pat, domain, createProject)
if err != nil {
return "", err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
defer resp.Body.Close()
p := new(project)
if err := json.Unmarshal(data, p); err != nil {
return "", err
}
fmt.Printf("projectID: %+v\n", p.ID)
return p.ID, nil
}
type createApp struct {
Name string `json:"name"`
RedirectUris []string `json:"redirectUris"`
ResponseTypes []string `json:"responseTypes"`
GrantTypes []string `json:"grantTypes"`
AppType string `json:"appType"`
AuthMethodType string `json:"authMethodType"`
PostLogoutRedirectUris []string `json:"postLogoutRedirectUris"`
Version string `json:"version"`
DevMode bool `json:"devMode"`
AccessTokenType string `json:"accessTokenType"`
AccessTokenRoleAssertion bool `json:"accessTokenRoleAssertion"`
IdTokenRoleAssertion bool `json:"idTokenRoleAssertion"`
IdTokenUserinfoAssertion bool `json:"idTokenUserinfoAssertion"`
ClockSkew string `json:"clockSkew"`
AdditionalOrigins []string `json:"additionalOrigins"`
SkipNativeAppSuccessPage bool `json:"skipNativeAppSuccessPage"`
BackChannelLogoutUri []string `json:"backChannelLogoutUri"`
LoginVersion version `json:"loginVersion"`
}
type version struct {
LoginV2 loginV2 `json:"loginV2"`
}
type loginV2 struct {
BaseUri string `json:"baseUri"`
}
type app struct {
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
}
func CreateApp(apiURL, pat, domain, projectID string, redirectURI, loginURL string) (string, string, error) {
createApp := &createApp{
Name: "OIDC",
RedirectUris: []string{redirectURI},
ResponseTypes: []string{"OIDC_RESPONSE_TYPE_CODE"},
GrantTypes: []string{"OIDC_GRANT_TYPE_AUTHORIZATION_CODE"},
AppType: "OIDC_APP_TYPE_WEB",
AuthMethodType: "OIDC_AUTH_METHOD_TYPE_BASIC",
Version: "OIDC_VERSION_1_0",
DevMode: true,
AccessTokenType: "OIDC_TOKEN_TYPE_BEARER",
AccessTokenRoleAssertion: true,
IdTokenRoleAssertion: true,
IdTokenUserinfoAssertion: true,
ClockSkew: "1s",
SkipNativeAppSuccessPage: true,
LoginVersion: version{
LoginV2: loginV2{
BaseUri: loginURL,
},
},
}
resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects/"+projectID+"/apps/oidc", pat, domain, createApp)
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", err
}
defer resp.Body.Close()
a := new(app)
if err := json.Unmarshal(data, a); err != nil {
return "", "", err
}
return a.ClientID, a.ClientSecret, err
}
func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data)))
if err != nil {
return nil, err
}
values := http.Header{}
values.Add("Authorization", "Bearer "+pat)
values.Add("x-forwarded-host", domain)
values.Add("Content-Type", "application/json")
req.Header = values
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}

View File

@@ -0,0 +1,22 @@
services:
samlsp:
image: golang:1.24-alpine
container_name: samlsp
command: go run main.go
environment:
API_URL: 'http://localhost:8080'
API_DOMAIN: 'localhost:8080'
PAT_FILE: '/pat/zitadel-admin-sa.pat'
LOGIN_URL: 'http://localhost:3000'
IDP_URL: 'http://localhost:3000/saml/v2/metadata'
HOST: 'http://localhost'
PORT: '8001'
working_dir: /saml
ports:
- 8001:8001
volumes:
- "../pat:/pat"
- "./:/saml"
extra_hosts:
- "localhost:host-gateway"

18
acceptance/saml/go.mod Normal file
View File

@@ -0,0 +1,18 @@
module github.com/zitadel/typescript/acceptance/saml
go 1.24.0
require github.com/crewjam/saml v0.4.14
require (
github.com/beevik/etree v1.5.0 // indirect
github.com/crewjam/httperr v0.2.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/russellhaering/goxmldsig v1.5.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
)

38
acceptance/saml/go.sum Normal file
View File

@@ -0,0 +1,38 @@
github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs=
github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
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/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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
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/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/russellhaering/goxmldsig v1.5.0 h1:AU2UkkYIUOTyZRbe08XMThaOCelArgvNfYapcmSjBNw=
github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvDP6tE0nTaeUnjXDmk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=

266
acceptance/saml/main.go Normal file
View File

@@ -0,0 +1,266 @@
package main
import (
"bytes"
"context"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/crewjam/saml/samlsp"
)
var keyPair = func() tls.Certificate {
cert := []byte(`-----BEGIN CERTIFICATE-----
MIIDITCCAgmgAwIBAgIUKjAUmxsHO44X+/TKBNciPgNl1GEwDQYJKoZIhvcNAQEL
BQAwIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1wbGUuY29tMB4XDTI0MTIxOTEz
Mzc1MVoXDTI1MTIxOTEzMzc1MVowIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1w
bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0QYuJsayILRI
hVT7G1DlitVSXnt1iw3gEXJZfe81Egz06fUbvXF6Yo1LJmwYpqe/rm+hf4FNUb8e
2O+LH2FieA9FkVe4P2gKOzw87A/KxvpV8stgNgl4LlqRCokbc1AzeE/NiLr5TcTD
RXm3DUcYxXxinprtDu2jftFysaOZmNAukvE/iL6qS3X6ggVEDDM7tY9n5FV2eJ4E
p0ImKfypi2aZYROxOK+v5x9ryFRMl4y07lMDvmtcV45uXYmfGNCgG9PNf91Kk/mh
JxEQbxycJwFoSi9XWljR8ahPdO11LXG7Dsj/RVbY8k2LdKNstl6Ae3aCpbe9u2Pj
vxYs1bVJuQIDAQABo1MwUTAdBgNVHQ4EFgQU+mRVN5HYJWgnpopReaLhf2cMcoYw
HwYDVR0jBBgwFoAU+mRVN5HYJWgnpopReaLhf2cMcoYwDwYDVR0TAQH/BAUwAwEB
/zANBgkqhkiG9w0BAQsFAAOCAQEABJpHVuc9tGhD04infRVlofvqXIUizTlOrjZX
vozW9pIhSWEHX8o+sJP8AMZLnrsdq+bm0HE0HvgYrw7Lb8pd4FpR46TkFHjeukoj
izqfgckjIBl2nwPGlynbKA0/U/rTCSxVt7XiAn+lgYUGIpOzNdk06/hRMitrMNB7
t2C97NseVC4b1ZgyFrozsefCfUmD8IJF0+XJ4Wzmsh0jRrI8koCtVmPYnKn6vw1b
cZprg/97CWHYrsavd406wOB60CMtYl83Q16ucOF1dretDFqJC5kY+aFLvuqfag2+
kIaoPV1MnGsxveQyyHdOsEatS5XOv/1OWcmnvePDPxcvb9jCcw==
-----END CERTIFICATE-----
`)
key := []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRBi4mxrIgtEiF
VPsbUOWK1VJee3WLDeARcll97zUSDPTp9Ru9cXpijUsmbBimp7+ub6F/gU1Rvx7Y
74sfYWJ4D0WRV7g/aAo7PDzsD8rG+lXyy2A2CXguWpEKiRtzUDN4T82IuvlNxMNF
ebcNRxjFfGKemu0O7aN+0XKxo5mY0C6S8T+IvqpLdfqCBUQMMzu1j2fkVXZ4ngSn
QiYp/KmLZplhE7E4r6/nH2vIVEyXjLTuUwO+a1xXjm5diZ8Y0KAb081/3UqT+aEn
ERBvHJwnAWhKL1daWNHxqE907XUtcbsOyP9FVtjyTYt0o2y2XoB7doKlt727Y+O/
FizVtUm5AgMBAAECggEACak+l5f6Onj+u5vrjc4JyAaXW6ra6loSM9g8Uu3sHukW
plwoA7Pzp0u20CAxrP1Gpqw984/hSCCcb0Q2ItWMWLaC/YZni5W2WFnOyo3pzlPa
hmH4UNMT+ReCSfF/oW8w69QLcNEMjhfEu0i2iWBygIlA4SoRwC2Db6yEX7nLMwUB
6AICid9hfeACNRz/nq5ytdcHdmcB7Ptgb9jLiXr6RZw26g5AsRPHU3LdcyZAOXjP
aUHriHuHQFKAVkoEUxslvCB6ePCTCpB0bSAuzQbeGoY8fmvmNSCvJ1vrH5hiSUYp
Axtl5iNgFl5o9obb0eBYlY9x3pMSz0twdbCwfR7HAQKBgQDtWhmFm0NaJALoY+tq
lIIC0EOMSrcRIlgeXr6+g8womuDOMi5m/Nr5Mqt4mPOdP4HytrQb+a/ZmEm17KHh
mQb1vwH8ffirCBHbPNC1vwSNoxDKv9E6OysWlKiOzxPFSVZr3dKl2EMX6qi17n0l
LBrGXXaNPgYiHSmwBA5CZvvouQKBgQDhclGJfZfuoubQkUuz8yOA2uxalh/iUmQ/
G8ac6/w7dmnL9pXehqCWh06SeC3ZvW7yrf7IIGx4sTJji2FzQ+8Ta6pPELMyBEXr
1VirIFrlNVMlMQEbZcbzdzEhchM1RUpZJtl3b4amvH21UcRB69d9klcDRisKoFRm
k0P9QLHpAQKBgQDh5J9nphZa4u0ViYtTW1XFIbs3+R/0IbCl7tww67TRbF3KQL4i
7EHna88ALumkXf3qJvKRsXgoaqS0jSqgUAjst8ZHLQkOldaQxneIkezedDSWEisp
9YgTrJYjnHefiyXB8VL63jE0wPOiewEF8Mzmv6sFz+L8cq7rQ2Di16qmmQKBgQDH
bvCwVxkrMpJK2O2GH8U9fOzu6bUE6eviY/jb4mp8U7EdjGJhuuieoM2iBoxQ/SID
rmYftYcfcWlo4+juJZ99p5W+YcCTs3IDQPUyVOnzr6uA0Avxp6RKxhsBQj+5tTUj
Dpn77P3JzB7MYqvhwPcdD3LH46+5s8FWCFpx02RPAQKBgARbngtggfifatcsMC7n
lSv/FVLH7LYQAHdoW/EH5Be7FeeP+eQvGXwh1dgl+u0VZO8FvI8RwFganpBRR2Nc
ZSBRIb0fSUlTvIsckSWjpEvUJUomJXyi4PIZAfNvd9/u1uLInQiCDtObwb6hnLTU
FHHEZ+dR4eMaJp6PhNm8hu2O
-----END PRIVATE KEY-----
`)
kp, err := tls.X509KeyPair(cert, key)
if err != nil {
panic(err)
}
kp.Leaf, err = x509.ParseCertificate(kp.Certificate[0])
if err != nil {
panic(err)
}
return kp
}()
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", samlsp.AttributeFromContext(r.Context(), "UserName"))
}
func main() {
apiURL := os.Getenv("API_URL")
pat := readPAT(os.Getenv("PAT_FILE"))
domain := os.Getenv("API_DOMAIN")
loginURL := os.Getenv("LOGIN_URL")
idpURL := os.Getenv("IDP_URL")
host := os.Getenv("HOST")
port := os.Getenv("PORT")
idpMetadataURL, err := url.Parse(idpURL)
if err != nil {
panic(err)
}
idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient,
*idpMetadataURL)
if err != nil {
panic(err)
}
fmt.Printf("idpMetadata: %+v\n", idpMetadata)
rootURL, err := url.Parse(host + ":" + port)
if err != nil {
panic(err)
}
samlSP, err := samlsp.New(samlsp.Options{
URL: *rootURL,
Key: keyPair.PrivateKey.(*rsa.PrivateKey),
Certificate: keyPair.Leaf,
IDPMetadata: idpMetadata,
})
if err != nil {
panic(err)
}
server := &http.Server{
Addr: ":" + port,
}
app := http.HandlerFunc(hello)
http.Handle("/hello", samlSP.RequireAccount(app))
http.Handle("/saml/", samlSP)
go func() {
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP server error: %v", err)
}
log.Println("Stopped serving new connections.")
}()
metadata, err := xml.MarshalIndent(samlSP.ServiceProvider.Metadata(), "", " ")
if err != nil {
panic(err)
}
if err := createZitadelResources(apiURL, pat, domain, metadata, loginURL); err != nil {
panic(err)
}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
}
func readPAT(path string) string {
f, err := os.Open(path)
if err != nil {
panic(err)
}
pat, err := io.ReadAll(f)
if err != nil {
panic(err)
}
return strings.Trim(string(pat), "\n")
}
func createZitadelResources(apiURL, pat, domain string, metadata []byte, loginURL string) error {
projectID, err := CreateProject(apiURL, pat, domain)
if err != nil {
return err
}
return CreateApp(apiURL, pat, domain, projectID, metadata, loginURL)
}
type project struct {
ID string `json:"id"`
}
type createProject struct {
Name string `json:"name"`
ProjectRoleAssertion bool `json:"projectRoleAssertion"`
ProjectRoleCheck bool `json:"projectRoleCheck"`
HasProjectCheck bool `json:"hasProjectCheck"`
PrivateLabelingSetting string `json:"privateLabelingSetting"`
}
func CreateProject(apiURL, pat, domain string) (string, error) {
createProject := &createProject{
Name: "SAML",
ProjectRoleAssertion: false,
ProjectRoleCheck: false,
HasProjectCheck: false,
PrivateLabelingSetting: "PRIVATE_LABELING_SETTING_UNSPECIFIED",
}
resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects", pat, domain, createProject)
if err != nil {
return "", err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
defer resp.Body.Close()
p := new(project)
if err := json.Unmarshal(data, p); err != nil {
return "", err
}
return p.ID, nil
}
type createApp struct {
Name string `json:"name"`
MetadataXml string `json:"metadataXml"`
LoginVersion version `json:"loginVersion"`
}
type version struct {
LoginV2 loginV2 `json:"loginV2"`
}
type loginV2 struct {
BaseUri string `json:"baseUri"`
}
func CreateApp(apiURL, pat, domain, projectID string, spMetadata []byte, loginURL string) error {
encoded := make([]byte, base64.URLEncoding.EncodedLen(len(spMetadata)))
base64.URLEncoding.Encode(encoded, spMetadata)
createApp := &createApp{
Name: "SAML",
MetadataXml: string(encoded),
LoginVersion: version{
LoginV2: loginV2{
BaseUri: loginURL,
},
},
}
_, err := doRequestWithHeaders(apiURL+"/management/v1/projects/"+projectID+"/apps/saml", pat, domain, createApp)
return err
}
func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data)))
if err != nil {
return nil, err
}
values := http.Header{}
values.Add("Authorization", "Bearer "+pat)
values.Add("x-forwarded-host", domain)
values.Add("Content-Type", "application/json")
req.Header = values
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}

View File

@@ -17,6 +17,40 @@ if [ -z "${PAT}" ]; then
PAT=$(cat ${PAT_FILE})
fi
#################################################################
# ServiceAccount as Login Client
#################################################################
SERVICEACCOUNT_RESPONSE=$(curl -s --request POST \
--url "${ZITADEL_API_INTERNAL_URL}/management/v1/users/machine" \
--header "Authorization: Bearer ${PAT}" \
--header "Host: ${ZITADEL_API_DOMAIN}" \
--header "Content-Type: application/json" \
-d "{\"userName\": \"login\", \"name\": \"Login v2\", \"description\": \"Serviceaccount for Login v2\", \"accessTokenType\": \"ACCESS_TOKEN_TYPE_BEARER\"}")
echo "Received ServiceAccount response: ${SERVICEACCOUNT_RESPONSE}"
SERVICEACCOUNT_ID=$(echo ${SERVICEACCOUNT_RESPONSE} | jq -r '. | .userId')
echo "Received ServiceAccount ID: ${SERVICEACCOUNT_ID}"
MEMBER_RESPONSE=$(curl -s --request POST \
--url "${ZITADEL_API_INTERNAL_URL}/admin/v1/members" \
--header "Authorization: Bearer ${PAT}" \
--header "Host: ${ZITADEL_API_DOMAIN}" \
--header "Content-Type: application/json" \
-d "{\"userId\": \"${SERVICEACCOUNT_ID}\", \"roles\": [\"IAM_LOGIN_CLIENT\"]}")
echo "Received Member response: ${MEMBER_RESPONSE}"
SA_PAT_RESPONSE=$(curl -s --request POST \
--url "${ZITADEL_API_INTERNAL_URL}/management/v1/users/${SERVICEACCOUNT_ID}/pats" \
--header "Authorization: Bearer ${PAT}" \
--header "Host: ${ZITADEL_API_DOMAIN}" \
--header "Content-Type: application/json" \
-d "{\"expirationDate\": \"2519-04-01T08:45:00.000000Z\"}")
echo "Received Member response: ${MEMBER_RESPONSE}"
SA_PAT=$(echo ${SA_PAT_RESPONSE} | jq -r '. | .token')
echo "Received ServiceAccount Token: ${SA_PAT}"
#################################################################
# Environment files
#################################################################
@@ -27,7 +61,8 @@ WRITE_TEST_ENVIRONMENT_FILE=${WRITE_TEST_ENVIRONMENT_FILE:-$(dirname "$0")/../ac
echo "Writing environment file to ${WRITE_TEST_ENVIRONMENT_FILE} when done."
echo "ZITADEL_API_URL=${ZITADEL_API_URL}
ZITADEL_SERVICE_USER_TOKEN=${PAT}
ZITADEL_SERVICE_USER_TOKEN=${SA_PAT}
ZITADEL_ADMIN_TOKEN=${PAT}
SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL}
EMAIL_VERIFICATION=true
DEBUG=true"| tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null

View File

@@ -1,3 +1,3 @@
module github.com/zitadel/typescript/acceptance/sink
go 1.22.6
go 1.24.0

View File

@@ -29,7 +29,7 @@ export async function passwordScreen(page: Page, password: string) {
export async function passwordScreenExpect(page: Page, password: string) {
await expect(page.getByTestId(passwordField)).toHaveValue(password);
await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify password");
await expect(page.getByTestId("error").locator("div")).toContainText("Failed to authenticate.");
}
export async function changePasswordScreenExpect(

View File

@@ -0,0 +1,5 @@
import { Page } from "@playwright/test";
export async function selectNewAccount(page: Page) {
await page.getByRole("link", { name: "Add another account" }).click();
}

View File

@@ -53,7 +53,7 @@ async function deleteCall(url: string) {
try {
const response = await axios.delete(url, {
headers: {
Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`,
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
},
});
@@ -87,7 +87,7 @@ async function listCall(url: string, data: any): Promise<any> {
const response = await axios.post(url, data, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`,
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
},
});
@@ -123,7 +123,7 @@ async function pushCall(url: string, data: any) {
const response = await axios.post(url, data, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`,
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
},
});

View File

@@ -4,7 +4,7 @@ import { linkingFailed } from "@/components/idps/pages/linking-failed";
import { linkingSuccess } from "@/components/idps/pages/linking-success";
import { loginFailed } from "@/components/idps/pages/login-failed";
import { loginSuccess } from "@/components/idps/pages/login-success";
import { idpTypeToIdentityProviderType, PROVIDER_MAPPING } from "@/lib/idp";
import { idpTypeToIdentityProviderType } from "@/lib/idp";
import { getServiceUrlFromHeaders } from "@/lib/service";
import {
addHuman,
@@ -19,10 +19,7 @@ import {
import { create } from "@zitadel/client";
import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb";
import {
AddHumanUserRequest,
AddHumanUserRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { AddHumanUserRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";
@@ -58,6 +55,7 @@ export default async function Page(props: {
});
const { idpInformation, userId } = intent;
let { addHumanUser } = intent;
// sign in user. If user should be linked continue
if (userId && !link) {
@@ -124,7 +122,7 @@ export default async function Page(props: {
// search for potential user via username, then link
if (options?.isLinkingAllowed) {
let foundUser;
const email = PROVIDER_MAPPING[providerType](idpInformation).email?.email;
const email = addHumanUser?.email?.email;
if (options.autoLinking === AutoLinkingOption.EMAIL && email) {
foundUser = await listUsers({ serviceUrl, email }).then((response) => {
@@ -180,16 +178,14 @@ export default async function Page(props: {
if (options?.isCreationAllowed && options.isAutoCreation) {
let orgToRegisterOn: string | undefined = organization;
let userData: AddHumanUserRequest =
PROVIDER_MAPPING[providerType](idpInformation);
let newUser;
if (
!orgToRegisterOn &&
userData.username && // username or email?
ORG_SUFFIX_REGEX.test(userData.username)
addHumanUser?.username && // username or email?
ORG_SUFFIX_REGEX.test(addHumanUser.username)
) {
const matched = ORG_SUFFIX_REGEX.exec(userData.username);
const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username);
const suffix = matched?.[1] ?? "";
// this just returns orgs where the suffix is set as primary domain
@@ -209,21 +205,21 @@ export default async function Page(props: {
}
}
if (orgToRegisterOn) {
if (addHumanUser && orgToRegisterOn) {
const organizationSchema = create(OrganizationSchema, {
org: { case: "orgId", value: orgToRegisterOn },
});
userData = create(AddHumanUserRequestSchema, {
...userData,
const addHumanUserWithOrganization = create(AddHumanUserRequestSchema, {
...addHumanUser,
organization: organizationSchema,
});
}
const newUser = await addHuman({
serviceUrl,
request: userData,
});
newUser = await addHuman({
serviceUrl,
request: addHumanUserWithOrganization,
});
}
if (newUser) {
return (

View File

@@ -0,0 +1,43 @@
"use client";
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
export default function SamlPost() {
const searchParams = useSearchParams();
const url = searchParams.get("url");
const relayState = searchParams.get("RelayState");
const samlResponse = searchParams.get("SAMLResponse");
useEffect(() => {
// Automatically submit the form after rendering
const form = document.getElementById("samlForm") as HTMLFormElement;
if (form) {
form.submit();
}
}, []);
if (!url || !relayState || !samlResponse) {
return (
<p className="text-center">Missing required parameters for SAML POST.</p>
);
}
return (
<html lang="en">
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Redirecting...</title>
</head>
<body>
<form id="samlForm" action={url} method="POST">
<input type="hidden" name="RelayState" value={relayState} />
<input type="hidden" name="SAMLResponse" value={samlResponse} />
</form>
<p>Redirecting...</p>
</body>
</html>
);
}

View File

@@ -473,40 +473,16 @@ export async function GET(request: NextRequest) {
if (url && binding.case === "redirect") {
return NextResponse.redirect(url);
} else if (url && binding.case === "post") {
// Create form data after SAML standard
const formData = {
RelayState: binding.value.relayState,
SAMLResponse: binding.value.samlResponse,
};
const redirectUrl = constructUrl(request, "/saml-post");
// Convert form data to URL-encoded string
const formBody = Object.entries(formData)
.map(
([key, value]) =>
encodeURIComponent(key) + "=" + encodeURIComponent(value),
)
.join("&");
redirectUrl.searchParams.set("url", url);
redirectUrl.searchParams.set("RelayState", binding.value.relayState);
redirectUrl.searchParams.set(
"SAMLResponse",
binding.value.samlResponse,
);
// Make a POST request to the external URL with the form data
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formBody,
});
// Handle the response from the external URL
if (response.ok) {
return NextResponse.json({
message: "SAML request completed successfully",
});
} else {
return NextResponse.json(
{ error: "Failed to complete SAML request" },
{ status: response.status },
);
}
return NextResponse.redirect(redirectUrl.toString());
} else {
console.log(
"could not create response, redirect user to choose other account",

View File

@@ -88,6 +88,7 @@ export function SignInWithIdp({
),
[IdentityProviderType.GITLAB]: SignInWithGitlab,
[IdentityProviderType.GITLAB_SELF_HOSTED]: SignInWithGitlab,
[IdentityProviderType.SAML]: SignInWithGeneric,
};
const Component = components[type];

View File

@@ -1,11 +1,5 @@
import { create } from "@zitadel/client";
import { IDPType } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import {
AddHumanUserRequest,
AddHumanUserRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
// This maps the IdentityProviderType to a slug which is used in the /success and /failure routes
export function idpTypeToSlug(idpType: IdentityProviderType) {
@@ -74,189 +68,3 @@ export function idpTypeToIdentityProviderType(
throw new Error("Unknown identity provider type");
}
}
// this maps the IDPInformation to the AddHumanUserRequest which is used when creating a user or linking a user (email)
// TODO: extend this object from a other file which can be overwritten by customers like map = { ...PROVIDER_MAPPING, ...customerMap }
export type OIDC_USER = {
User: {
email: string;
name?: string;
given_name?: string;
family_name?: string;
};
};
const GITLAB_MAPPING = (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as {
name: string;
email: string;
email_verified: boolean;
};
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.email,
verification: { case: "isVerified", value: rawInfo.email_verified },
},
profile: {
displayName: rawInfo.name || idp.userName || "",
givenName: "",
familyName: "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
};
const OIDC_MAPPING = (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as OIDC_USER;
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.User?.email,
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.User?.name ?? "",
givenName: rawInfo.User?.given_name ?? "",
familyName: rawInfo.User?.family_name ?? "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
};
const GITHUB_MAPPING = (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as {
email: string;
name: string;
};
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.email,
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.name ?? "",
givenName: rawInfo.name ?? "",
familyName: rawInfo.name ?? "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
};
export const PROVIDER_MAPPING: {
[provider: number]: (rI: IDPInformation) => AddHumanUserRequest;
} = {
[IdentityProviderType.GOOGLE]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as OIDC_USER;
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.User?.email,
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.User?.name ?? "",
givenName: rawInfo.User?.given_name ?? "",
familyName: rawInfo.User?.family_name ?? "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
},
[IdentityProviderType.GITLAB]: GITLAB_MAPPING,
[IdentityProviderType.GITLAB_SELF_HOSTED]: GITLAB_MAPPING,
[IdentityProviderType.OIDC]: OIDC_MAPPING,
// check
[IdentityProviderType.OAUTH]: OIDC_MAPPING,
[IdentityProviderType.AZURE_AD]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as {
jobTitle: string;
mail: string;
mobilePhone: string;
preferredLanguage: string;
id: string;
displayName?: string;
givenName?: string;
surname?: string;
officeLocation?: string;
userPrincipalName: string;
};
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.mail || rawInfo.userPrincipalName || "",
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.displayName ?? "",
givenName: rawInfo.givenName ?? "",
familyName: rawInfo.surname ?? "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
},
[IdentityProviderType.GITHUB]: GITHUB_MAPPING,
[IdentityProviderType.GITHUB_ES]: GITHUB_MAPPING,
[IdentityProviderType.APPLE]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as {
name?: string;
firstName?: string;
lastName?: string;
email?: string;
};
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.email ?? "",
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.name ?? "",
givenName: rawInfo.firstName ?? "",
familyName: rawInfo.lastName ?? "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
},
};

View File

@@ -915,27 +915,6 @@ export async function startIdentityProviderFlow({
});
}
export async function retrieveIdentityProviderInformation({
serviceUrl,
idpIntentId,
idpIntentToken,
}: {
serviceUrl: string;
idpIntentId: string;
idpIntentToken: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
UserService,
serviceUrl,
);
return userService.retrieveIdentityProviderIntent({
idpIntentId,
idpIntentToken,
});
}
export async function getAuthRequest({
serviceUrl,
authRequestId,

View File

@@ -22,7 +22,9 @@ export async function middleware(request: NextRequest) {
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const instanceHost = `${serviceUrl}`.replace("https://", "");
const instanceHost = `${serviceUrl}`
.replace("https://", "")
.replace("http://", "");
const requestHeaders = new Headers(request.headers);

View File

@@ -26,6 +26,10 @@
"release": "turbo run build --filter=login^... && changeset publish",
"run-zitadel": "docker compose -f ./acceptance/docker-compose.yaml run setup",
"run-sink": "docker compose -f ./acceptance/docker-compose.yaml up -d sink",
"run-samlsp": "docker compose -f ./acceptance/saml/docker-compose.yaml up -d",
"run-samlidp": "docker compose -f ./acceptance/idp/saml/docker-compose.yaml up -d",
"run-oidcrp": "docker compose -f ./acceptance/oidc/docker-compose.yaml up -d",
"run-oidcop": "docker compose -f ./acceptance/idp/oidc/docker-compose.yaml up -d",
"stop": "docker compose -f ./acceptance/docker-compose.yaml stop"
},
"pnpm": {

View File

@@ -14,7 +14,7 @@
],
"sideEffects": false,
"scripts": {
"generate": "buf generate https://github.com/zitadel/zitadel.git#tag=v2.71.1 --path ./proto/zitadel",
"generate": "buf generate https://github.com/zitadel/zitadel.git#ref=02617cf17fdde849378c1a6b5254bbfb2745b164 --path ./proto/zitadel",
"clean": "rm -rf zitadel .turbo node_modules google protoc-gen-openapiv2 validate"
},
"dependencies": {