From 5af009d58134bb1217d821397e7674f2f22786a2 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Fri, 21 Mar 2025 17:33:42 +0100 Subject: [PATCH] chore: add acceptance tests with saml sp --- .github/workflows/test.yml | 4 + acceptance/Dockerfile | 2 +- acceptance/docker-compose.yaml | 6 +- acceptance/saml/Dockerfile | 5 + acceptance/saml/docker-compose.yaml | 35 ++++++ acceptance/saml/go.mod | 16 +++ acceptance/saml/go.sum | 63 ++++++++++ acceptance/saml/main.go | 118 ++++++++++++++++++ acceptance/saml/setup.sh | 36 ++++++ acceptance/setup.sh | 37 +++++- acceptance/sink/go.mod | 2 +- .../tests/saml-username-password.spec.ts | 39 ++++++ acceptance/tests/saml.ts | 5 + acceptance/tests/select-account.ts | 5 + acceptance/tests/zitadel.ts | 6 +- apps/login/src/app/login/route.ts | 9 +- apps/login/src/middleware.ts | 2 +- package.json | 1 + 18 files changed, 375 insertions(+), 16 deletions(-) create mode 100644 acceptance/saml/Dockerfile create mode 100644 acceptance/saml/docker-compose.yaml create mode 100644 acceptance/saml/go.mod create mode 100644 acceptance/saml/go.sum create mode 100644 acceptance/saml/main.go create mode 100755 acceptance/saml/setup.sh create mode 100644 acceptance/tests/saml-username-password.spec.ts create mode 100644 acceptance/tests/saml.ts create mode 100644 acceptance/tests/select-account.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 79a455b016..05c8052139 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/acceptance/Dockerfile b/acceptance/Dockerfile index 36f6ba8f19..dd29721bc3 100644 --- a/acceptance/Dockerfile +++ b/acceptance/Dockerfile @@ -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 diff --git a/acceptance/docker-compose.yaml b/acceptance/docker-compose.yaml index 240d91553a..61bcec04db 100644 --- a/acceptance/docker-compose.yaml +++ b/acceptance/docker-compose.yaml @@ -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: diff --git a/acceptance/saml/Dockerfile b/acceptance/saml/Dockerfile new file mode 100644 index 0000000000..dd29721bc3 --- /dev/null +++ b/acceptance/saml/Dockerfile @@ -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" ] diff --git a/acceptance/saml/docker-compose.yaml b/acceptance/saml/docker-compose.yaml new file mode 100644 index 0000000000..19b3473e28 --- /dev/null +++ b/acceptance/saml/docker-compose.yaml @@ -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" \ No newline at end of file diff --git a/acceptance/saml/go.mod b/acceptance/saml/go.mod new file mode 100644 index 0000000000..d3378ea0b3 --- /dev/null +++ b/acceptance/saml/go.mod @@ -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 +) diff --git a/acceptance/saml/go.sum b/acceptance/saml/go.sum new file mode 100644 index 0000000000..482277f9de --- /dev/null +++ b/acceptance/saml/go.sum @@ -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= diff --git a/acceptance/saml/main.go b/acceptance/saml/main.go new file mode 100644 index 0000000000..273e93c115 --- /dev/null +++ b/acceptance/saml/main.go @@ -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) +} diff --git a/acceptance/saml/setup.sh b/acceptance/saml/setup.sh new file mode 100755 index 0000000000..9d8f68ccac --- /dev/null +++ b/acceptance/saml/setup.sh @@ -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}" diff --git a/acceptance/setup.sh b/acceptance/setup.sh index 8438685dde..cdb04043e0 100755 --- a/acceptance/setup.sh +++ b/acceptance/setup.sh @@ -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 diff --git a/acceptance/sink/go.mod b/acceptance/sink/go.mod index a33d6ae8bd..1da7622b58 100644 --- a/acceptance/sink/go.mod +++ b/acceptance/sink/go.mod @@ -1,3 +1,3 @@ module github.com/zitadel/typescript/acceptance/sink -go 1.22.6 +go 1.24.0 diff --git a/acceptance/tests/saml-username-password.spec.ts b/acceptance/tests/saml-username-password.spec.ts new file mode 100644 index 0000000000..a7f1864317 --- /dev/null +++ b/acceptance/tests/saml-username-password.spec.ts @@ -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 +}); diff --git a/acceptance/tests/saml.ts b/acceptance/tests/saml.ts new file mode 100644 index 0000000000..ee9b8ced32 --- /dev/null +++ b/acceptance/tests/saml.ts @@ -0,0 +1,5 @@ +import { expect, Page } from "@playwright/test"; + +export async function startSAML(page: Page) { + await page.goto("http://localhost:8001/hello"); +} \ No newline at end of file diff --git a/acceptance/tests/select-account.ts b/acceptance/tests/select-account.ts new file mode 100644 index 0000000000..ce036ab39b --- /dev/null +++ b/acceptance/tests/select-account.ts @@ -0,0 +1,5 @@ +import {Page} from "@playwright/test"; + +export async function selectNewAccount(page: Page) { + await page.getByRole('link', {name: 'Add another account'}).click(); +} diff --git a/acceptance/tests/zitadel.ts b/acceptance/tests/zitadel.ts index 1923b63dce..ae29bf84e5 100644 --- a/acceptance/tests/zitadel.ts +++ b/acceptance/tests/zitadel.ts @@ -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 { 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}`, }, }); diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 184e1919bf..65b1b2eae7 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -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 { diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index e5cbf7ad3f..a1fd47504a 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -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); diff --git a/package.json b/package.json index a824e47571..2a4868e720 100644 --- a/package.json +++ b/package.json @@ -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": {