chore: add acceptance tests with saml sp

This commit is contained in:
Stefan Benz
2025-03-21 17:33:42 +01:00
parent f1ab6a2360
commit 5af009d581
18 changed files with 375 additions and 16 deletions

View File

@@ -97,6 +97,10 @@ jobs:
run: ZITADEL_DEV_UID=root pnpm run-sink
if: ${{ matrix.command == 'test:acceptance' }}
- name: Run SAML SP
run: ZITADEL_DEV_UID=root pnpm run-samlsp
if: ${{ matrix.command == 'test:acceptance' }}
- name: Create Cloud Env File
run: |
if [ "${{ matrix.command }}" == "test:acceptance:prod" ]; then

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:dc64e35128108d70471c7a5b9ad1dfc2c7c4c654}"
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:
- "host.docker.internal: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,5 @@
FROM golang:1.24-alpine
RUN apk add curl jq
COPY setup.sh /setup.sh
RUN chmod +x /setup.sh
ENTRYPOINT [ "/setup.sh" ]

View File

@@ -0,0 +1,35 @@
services:
samlsp:
image: golang:1.24-alpine
container_name: samlsp
command: go run main.go -host 'http://localhost' -port '8001' -idp 'http://host.docker.internal:3000/saml/v2/metadata'
working_dir: /saml
ports:
- 8001:8001
volumes:
- "./:/saml"
extra_hosts:
- "host.docker.internal:host-gateway"
wait_for_samlsp:
image: curlimages/curl:8.00.1
command: /bin/sh -c "until curl -s -o /dev/null -i -f http://samlsp:8001/saml/metadata; do echo 'waiting' && sleep 1; done; echo 'ready' && sleep 5;" || false
depends_on:
- samlsp
setup-samlsp:
user: "${ZITADEL_DEV_UID}"
container_name: setup-samlsp
build: .
environment:
PAT_FILE: /pat/zitadel-admin-sa.pat
ZITADEL_API_URL: http://host.docker.internal:8080
LOGIN_URI: http://localhost:3000
SAML_SP_METADATA: http://host.docker.internal:8001/saml/metadata
volumes:
- "../pat:/pat"
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
wait_for_samlsp:
condition: "service_completed_successfully"

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

@@ -0,0 +1,16 @@
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/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
golang.org/x/crypto v0.36.0 // indirect
)

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

@@ -0,0 +1,63 @@
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/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs=
github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo=
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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/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.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/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
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.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/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/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.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
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.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.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=

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

@@ -0,0 +1,118 @@
package main
import (
"context"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"flag"
"fmt"
"net/http"
"net/url"
"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(), "displayName"))
}
func main() {
idpURL := flag.String("idp", "http://localhost:3000/saml/v2/metadata", "url to idp metadata, proxied through typescript")
host := flag.String("host", "http://localhost", "url for sp")
port := flag.String("port", "8001", "port for sp")
flag.Parse()
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)
}
app := http.HandlerFunc(hello)
http.Handle("/hello", samlSP.RequireAccount(app))
http.Handle("/saml/", samlSP)
http.ListenAndServe(":"+*port, nil)
}

36
acceptance/saml/setup.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/sh
set -ex
PAT_FILE=${PAT_FILE:-../pat/zitadel-admin-sa.pat}
ZITADEL_API_URL="${ZITADEL_API_URL:-"http://localhost:8080"}"
LOGIN_URI="${LOGIN_URI:-"http://localhost:3000"}"
SAML_SP_METADATA="${SAML_SP_METADATA:-"http://samlsp:8081/saml/metadata"}"
if [ -z "${PAT}" ]; then
echo "Reading PAT from file ${PAT_FILE}"
PAT=$(cat ${PAT_FILE})
fi
#################################################################
# SAML Application
#################################################################
SAML_PROJECT_RESPONSE=$(curl -s --request POST \
--url "${ZITADEL_API_URL}/management/v1/projects" \
--header "Authorization: Bearer ${PAT}" \
--header "Host: ${ZITADEL_API_DOMAIN}" \
--header "Content-Type: application/json" \
-d "{ \"name\": \"SAML\", \"projectRoleAssertion\": true, \"projectRoleCheck\": true, \"hasProjectCheck\": true, \"privateLabelingSetting\": \"PRIVATE_LABELING_SETTING_UNSPECIFIED\"}")
echo "Received SAML Project response: ${SAML_PROJECT_RESPONSE}"
SAML_PROJECT_ID=$(echo ${SAML_PROJECT_RESPONSE} | jq -r '. | .id')
echo "Received Project ID: ${SAML_PROJECT_ID}"
SAML_APP_RESPONSE=$(curl -s --request POST \
--url "${ZITADEL_API_URL}/management/v1/projects/${SAML_PROJECT_ID}/apps/saml" \
--header "Authorization: Bearer ${PAT}" \
--header "Host: ${ZITADEL_API_DOMAIN}" \
--header "Content-Type: application/json" \
-d "{ \"name\": \"SAML\", \"metadataUrl\": \"${SAML_SP_METADATA}\", \"loginVersion\": { \"loginV2\": { \"baseUri\": \"${LOGIN_URI}\" }}}")
echo "Received SAML App response: ${SAML_APP_RESPONSE}"

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

@@ -0,0 +1,39 @@
import { faker } from "@faker-js/faker";
import { test as base } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
import { loginname } from "./loginname";
import { password } from "./password";
import { PasswordUser } from "./user";
import {startSAML} from "./saml";
import {selectNewAccount} from "./select-account";
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, ".env.local") });
const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => {
const user = new PasswordUser({
email: faker.internet.email(),
isEmailVerified: true,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
organization: "",
phone: faker.phone.number(),
isPhoneVerified: false,
password: "Password1!",
passwordChangeRequired: false,
});
await user.ensure(page);
await use(user);
await user.cleanup();
},
});
test("saml username and password login", async ({ user, page }) => {
await startSAML(page)
await selectNewAccount(page)
await loginname(page, user.getUsername());
await password(page, user.getPassword());
// currently fails because of issuer problems
});

5
acceptance/tests/saml.ts Normal file
View File

@@ -0,0 +1,5 @@
import { expect, Page } from "@playwright/test";
export async function startSAML(page: Page) {
await page.goto("http://localhost:8001/hello");
}

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

@@ -473,16 +473,11 @@ export async function GET(request: NextRequest) {
if (url && binding.case === "redirect") {
return NextResponse.redirect(url);
} else if (url && binding.case === "post") {
const formData = {
RelayState: binding.value.relayState,
SAMLResponse: binding.value.samlResponse,
};
const redirectUrl = constructUrl(request, "/saml-post");
redirectUrl.searchParams.set("url", url);
redirectUrl.searchParams.set("RelayState", formData.RelayState);
redirectUrl.searchParams.set("SAMLResponse", formData.SAMLResponse);
redirectUrl.searchParams.set("RelayState", binding.value.relayState);
redirectUrl.searchParams.set("SAMLResponse", binding.value.samlResponse);
return NextResponse.redirect(redirectUrl.toString());
} else {

View File

@@ -22,7 +22,7 @@ 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,7 @@
"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",
"stop": "docker compose -f ./acceptance/docker-compose.yaml stop"
},
"pnpm": {