mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-13 11:40:16 +00:00
chore: add acceptance tests with oidc rp
This commit is contained in:
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -101,6 +101,10 @@ jobs:
|
||||
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: Create Cloud Env File
|
||||
run: |
|
||||
if [ "${{ matrix.command }}" == "test:acceptance:prod" ]; then
|
||||
|
22
acceptance/oidc/docker-compose.yaml
Normal file
22
acceptance/oidc/docker-compose.yaml
Normal 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"
|
27
acceptance/oidc/go.mod
Normal file
27
acceptance/oidc/go.mod
Normal file
@@ -0,0 +1,27 @@
|
||||
module github.com/zitadel/typescript/acceptance/oidc
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
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
|
||||
)
|
69
acceptance/oidc/go.sum
Normal file
69
acceptance/oidc/go.sum
Normal file
@@ -0,0 +1,69 @@
|
||||
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
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=
|
322
acceptance/oidc/main.go
Normal file
322
acceptance/oidc/main.go
Normal file
@@ -0,0 +1,322 @@
|
||||
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/joho/godotenv"
|
||||
"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() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
}
|
||||
|
||||
apiURL := os.Getenv("API_URL")
|
||||
patFile := 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"), " ")
|
||||
|
||||
f, err := os.Open(patFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pat, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
patStr := strings.Trim(string(pat), "\n")
|
||||
|
||||
redirectURI := fmt.Sprintf("%v:%v%v", host, port, callbackPath)
|
||||
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
|
||||
|
||||
clientID, clientSecret, err := createZitadelResources(apiURL, patStr, 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 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",
|
||||
}
|
||||
body, err := json.Marshal(createProject)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects", pat, domain, body)
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
body, err := json.Marshal(createApp)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects/"+projectID+"/apps/oidc", pat, domain, body)
|
||||
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 []byte) (*http.Response, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(body)))
|
||||
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
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
FROM golang:1.24-alpine
|
||||
RUN apk add curl jq
|
||||
COPY setup.sh /setup.sh
|
||||
RUN chmod +x /setup.sh
|
||||
ENTRYPOINT [ "/setup.sh" ]
|
@@ -2,34 +2,21 @@ 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'
|
||||
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:
|
||||
- "host.docker.internal:host-gateway"
|
||||
- "localhost: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"
|
@@ -8,9 +8,11 @@ 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
|
||||
)
|
||||
|
@@ -1,8 +1,5 @@
|
||||
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=
|
||||
@@ -10,53 +7,31 @@ github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1n
|
||||
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/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/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=
|
||||
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/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=
|
||||
|
@@ -1,14 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"flag"
|
||||
"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"
|
||||
)
|
||||
@@ -80,13 +91,25 @@ func hello(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
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")
|
||||
apiURL := os.Getenv("API_URL")
|
||||
patFile := 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")
|
||||
|
||||
flag.Parse()
|
||||
f, err := os.Open(patFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pat, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
patStr := strings.Trim(string(pat), "\n")
|
||||
|
||||
idpMetadataURL, err := url.Parse(*idpURL)
|
||||
idpMetadataURL, err := url.Parse(idpURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -96,7 +119,7 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("idpMetadata: %+v\n", idpMetadata)
|
||||
rootURL, err := url.Parse(*host + ":" + *port)
|
||||
rootURL, err := url.Parse(host + ":" + port)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -111,8 +134,136 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + port,
|
||||
}
|
||||
app := http.HandlerFunc(hello)
|
||||
http.Handle("/hello", samlSP.RequireAccount(app))
|
||||
http.Handle("/saml/", samlSP)
|
||||
http.ListenAndServe(":"+*port, nil)
|
||||
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, patStr, 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 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",
|
||||
}
|
||||
body, err := json.Marshal(createProject)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects", pat, domain, body)
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
body, err := json.Marshal(createApp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = doRequestWithHeaders(apiURL+"/management/v1/projects/"+projectID+"/apps/saml", pat, domain, body)
|
||||
return err
|
||||
}
|
||||
|
||||
func doRequestWithHeaders(apiURL, pat, domain string, body []byte) (*http.Response, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(body)))
|
||||
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
|
||||
}
|
||||
|
@@ -1,36 +0,0 @@
|
||||
#!/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}"
|
37
acceptance/tests/oidc-username-password.spec.ts
Normal file
37
acceptance/tests/oidc-username-password.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { test as base, expect } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
import { loginname } from "./loginname";
|
||||
import { password } from "./password";
|
||||
import { PasswordUser } from "./user";
|
||||
import {startOIDC} from "./oidc";
|
||||
|
||||
// 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("oidc username and password login", async ({ user, page }) => {
|
||||
await startOIDC(page)
|
||||
await loginname(page, user.getUsername());
|
||||
await password(page, user.getPassword());
|
||||
await expect(page.locator('pre')).toContainText(user.getUsername());
|
||||
});
|
5
acceptance/tests/oidc.ts
Normal file
5
acceptance/tests/oidc.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { expect, Page } from "@playwright/test";
|
||||
|
||||
export async function startOIDC(page: Page) {
|
||||
await page.goto("http://localhost:8000/login");
|
||||
}
|
@@ -27,6 +27,7 @@
|
||||
"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-oidcrp": "docker compose -f ./acceptance/oidc/docker-compose.yaml up -d",
|
||||
"stop": "docker compose -f ./acceptance/docker-compose.yaml stop"
|
||||
},
|
||||
"pnpm": {
|
||||
|
Reference in New Issue
Block a user