From 12273ecfe8663fdb50570036de74daa0881a531d Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sun, 7 Apr 2024 00:50:02 +0100 Subject: [PATCH] initial commit --- TODO.txt | 0 go.mod | 19 ++++++++++++ go.sum | 62 +++++++++++++++++++++++++++++++++++++ helpers.go | 57 ++++++++++++++++++++++++++++++++++ main.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++ settings.go | 15 +++++++++ 6 files changed, 241 insertions(+) create mode 100644 TODO.txt create mode 100644 go.mod create mode 100644 go.sum create mode 100644 helpers.go create mode 100644 main.go create mode 100644 settings.go diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..db5dcce --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/lukaszraczylo/traefik-oidc + +go 1.22.1 + +require ( + github.com/coreos/go-oidc/v3 v3.10.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/sessions v1.2.2 + golang.org/x/oauth2 v0.13.0 +) + +require ( + github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + golang.org/x/crypto v0.19.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.31.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..70ff8fb --- /dev/null +++ b/go.sum @@ -0,0 +1,62 @@ +github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= +github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= +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-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= +github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/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/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= +github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +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/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..8ba9708 --- /dev/null +++ b/helpers.go @@ -0,0 +1,57 @@ +package traefikoidc + +import ( + "net/http" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/gorilla/sessions" +) + +func (t *TraefikOidc) handleCallback(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + session, err := t.store.Get(req, "session-name") + if err != nil { + http.Error(rw, "Session error: "+err.Error(), http.StatusInternalServerError) + return + } + + // Compare the CSRF token from the session with the "state" parameter from the callback + callbackState := req.URL.Query().Get("state") + if sessionState, ok := session.Values["csrf"].(string); !ok || callbackState != sessionState { + http.Error(rw, "Invalid state parameter", http.StatusBadRequest) + return + } + + oauth2Token, err := t.oauthConfig.Exchange(ctx, req.URL.Query().Get("code")) + if err != nil { + http.Error(rw, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError) + return + } + + rawIDToken, ok := oauth2Token.Extra("id_token").(string) + if !ok { + http.Error(rw, "No id_token field in oauth2 token.", http.StatusInternalServerError) + return + } + + _, err = t.provider.Verifier(&oidc.Config{ClientID: t.oauthConfig.ClientID}).Verify(ctx, rawIDToken) + if err != nil { + http.Error(rw, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError) + return + } + + session.Values["authenticated"] = true + session.Values["id_token"] = rawIDToken + session.Options = &sessions.Options{ + Path: "/", + MaxAge: 3600, + HttpOnly: true, + Secure: true, // Ensure cookies are sent over HTTPS + } + err = session.Save(req, rw) + if err != nil { + http.Error(rw, "Failed to save session: "+err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..0b738ef --- /dev/null +++ b/main.go @@ -0,0 +1,88 @@ +package traefikoidc + +import ( + "context" + "log" + "net/http" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/google/uuid" + "github.com/gorilla/sessions" + "golang.org/x/oauth2" +) + +type TraefikOidc struct { + next http.Handler + name string + provider *oidc.Provider + oauthConfig oauth2.Config + store *sessions.CookieStore +} + +func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { + provider, err := oidc.NewProvider(ctx, config.ProviderURL) + if err != nil { + log.Fatal("Can't connect to the provider", err) + return nil, err + } + + store := sessions.NewCookieStore([]byte(config.SessionEncryptionKey)) + + oauthConfig := oauth2.Config{ + ClientID: config.ClientID, + ClientSecret: config.ClientSecret, + RedirectURL: config.CallbackURL, + Endpoint: provider.Endpoint(), + Scopes: append([]string{oidc.ScopeOpenID}, config.Scopes...), + } + + return &TraefikOidc{ + provider: provider, + oauthConfig: oauthConfig, + next: next, + name: name, + store: store, + }, nil +} + +func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if req.URL.Path == t.oauthConfig.RedirectURL { + t.handleCallback(rw, req) + return + } + + session, err := t.store.Get(req, "session-name") + if err != nil { + http.Error(rw, "Session error: "+err.Error(), http.StatusInternalServerError) + return + } + + if t.isUserAuthenticated(req) { + t.next.ServeHTTP(rw, req) + } + + csrfToken := uuid.New().String() + session.Values["csrf"] = csrfToken + err = session.Save(req, rw) + if err != nil { + http.Error(rw, "Failed to save session: "+err.Error(), http.StatusInternalServerError) + return + } + + // Use the CSRF token as the OIDC "state" parameter for CSRF protection + redirectURL := t.oauthConfig.AuthCodeURL(csrfToken, oidc.Nonce(uuid.New().String())) + http.Redirect(rw, req, redirectURL, http.StatusFound) +} + +func (t *TraefikOidc) isUserAuthenticated(req *http.Request) bool { + session, err := t.store.Get(req, "session-name") + if err != nil { + return false + } + + if auth, ok := session.Values["authenticated"].(bool); !ok || !auth { + return false + } + + return true +} diff --git a/settings.go b/settings.go new file mode 100644 index 0000000..5b23cf4 --- /dev/null +++ b/settings.go @@ -0,0 +1,15 @@ +package traefikoidc + +type Config struct { + ProviderURL string `json:"providerURL"` + CallbackURL string `json:"callbackURL"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + Scopes []string `json:"scopes"` + LogLevel string `json:"logLevel"` + SessionEncryptionKey string `json:"sessionEncryptionKey"` +} + +func CreateConfig() *Config { + return &Config{} +}