From f761900a3ebd2c5f181ea751099ceb9895bdb4d2 Mon Sep 17 00:00:00 2001 From: Jan Eitzinger Date: Wed, 13 Mar 2024 09:37:12 +0100 Subject: [PATCH 1/6] Add initial code for oidc authentication support --- go.mod | 16 +++++----- go.sum | 19 +++++++++++ internal/auth/auth.go | 19 ++++++----- internal/auth/oidc.go | 73 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 internal/auth/oidc.go diff --git a/go.mod b/go.mod index facfa00..4de12ec 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/swaggo/http-swagger v1.3.3 github.com/swaggo/swag v1.16.2 github.com/vektah/gqlparser/v2 v2.5.10 - golang.org/x/crypto v0.16.0 + golang.org/x/crypto v0.21.0 golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea ) @@ -37,15 +37,17 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/containerd/containerd v1.6.18 // indirect + github.com/coreos/go-oidc/v3 v3.9.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/deepmap/oapi-codegen v1.12.4 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect + github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/swag v0.22.4 // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.4.0 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect @@ -76,13 +78,13 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.uber.org/atomic v1.10.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/oauth2 v0.5.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/oauth2 v0.18.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.16.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index abd7f71..2658092 100644 --- a/go.sum +++ b/go.sum @@ -339,6 +339,8 @@ github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmeka github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= +github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -451,6 +453,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -588,6 +592,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 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/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -1288,6 +1294,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1411,6 +1419,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1431,6 +1441,8 @@ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1570,6 +1582,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1585,6 +1599,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/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/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= @@ -1732,6 +1747,8 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1854,6 +1871,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index e8f0db4..fe3edad 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -27,18 +27,18 @@ type Authenticator interface { } type Authentication struct { - sessionStore *sessions.CookieStore - SessionMaxAge time.Duration - - authenticators []Authenticator + sessionStore *sessions.CookieStore LdapAuth *LdapAuthenticator JwtAuth *JWTAuthenticator LocalAuth *LocalAuthenticator + authenticators []Authenticator + SessionMaxAge time.Duration } func (auth *Authentication) AuthViaSession( rw http.ResponseWriter, - r *http.Request) (*schema.User, error) { + r *http.Request, +) (*schema.User, error) { session, err := auth.sessionStore.Get(r, "session") if err != nil { log.Error("Error while getting session store") @@ -131,8 +131,8 @@ func Init() (*Authentication, error) { func (auth *Authentication) Login( onsuccess http.Handler, - onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler { - + onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error), +) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { username := r.FormValue("username") var dbUser *schema.User @@ -193,10 +193,9 @@ func (auth *Authentication) Login( func (auth *Authentication) Auth( onsuccess http.Handler, - onfailure func(rw http.ResponseWriter, r *http.Request, authErr error)) http.Handler { - + onfailure func(rw http.ResponseWriter, r *http.Request, authErr error), +) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - user, err := auth.JwtAuth.AuthViaJWT(rw, r) if err != nil { log.Infof("authentication failed: %s", err.Error()) diff --git a/internal/auth/oidc.go b/internal/auth/oidc.go new file mode 100644 index 0000000..480b212 --- /dev/null +++ b/internal/auth/oidc.go @@ -0,0 +1,73 @@ +// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package auth + +import ( + "context" + "log" + "net/http" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/gorilla/mux" + "golang.org/x/oauth2" +) + +type OIDC struct { + client *oauth2.Config + provider *oidc.Provider + state string + codeVerifier string +} + +func (oa *OIDC) Init(r *mux.Router) error { + oa.client = &oauth2.Config{ + ClientID: "YOUR_CLIENT_ID", + ClientSecret: "YOUR_CLIENT_SECRET", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://provider.com/o/oauth2/auth", + TokenURL: "https://provider.com/o/oauth2/token", + }, + } + + provider, err := oidc.NewProvider(context.Background(), "https://provider") + if err != nil { + log.Fatal(err) + } + + oa.provider = provider + + r.HandleFunc("/oidc-login", oa.OAuth2Login) + r.HandleFunc("/oidc-callback", oa.OAuth2Callback) + + return nil +} + +func (oa *OIDC) OAuth2Callback(rw http.ResponseWriter, r *http.Request) { + _ = r.ParseForm() + state := r.Form.Get("state") + if state != oa.state { + http.Error(rw, "State invalid", http.StatusBadRequest) + return + } + code := r.Form.Get("code") + if code == "" { + http.Error(rw, "Code not found", http.StatusBadRequest) + return + } + token, err := oa.client.Exchange(context.Background(), code, oauth2.VerifierOption(oa.codeVerifier)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } +} + +func (oa *OIDC) OAuth2Login(rw http.ResponseWriter, r *http.Request) { + // use PKCE to protect against CSRF attacks + oa.codeVerifier = oauth2.GenerateVerifier() + + // Redirect user to consent page to ask for permission + url := oa.client.AuthCodeURL("state", oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(oa.codeVerifier)) + http.Redirect(rw, r, url, http.StatusFound) +} From e92e727279b8cf006047b87031c733f46abc6438 Mon Sep 17 00:00:00 2001 From: Jan Eitzinger Date: Wed, 13 Mar 2024 17:09:36 +0100 Subject: [PATCH 2/6] Extend oidc auth provider --- internal/auth/ldap.go | 11 +++--- internal/auth/oidc.go | 84 ++++++++++++++++++++++++++++++++----------- 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/internal/auth/ldap.go b/internal/auth/ldap.go index b800ca7..d9888ca 100644 --- a/internal/auth/ldap.go +++ b/internal/auth/ldap.go @@ -21,7 +21,7 @@ import ( type LdapAuthenticator struct { syncPassword string - UserAttr string + UserAttr string } var _ Authenticator = (*LdapAuthenticator)(nil) @@ -74,8 +74,8 @@ func (la *LdapAuthenticator) CanLogin( user *schema.User, username string, rw http.ResponseWriter, - r *http.Request) (*schema.User, bool) { - + r *http.Request, +) (*schema.User, bool) { lc := config.Keys.LdapConfig if user != nil { @@ -138,8 +138,8 @@ func (la *LdapAuthenticator) CanLogin( func (la *LdapAuthenticator) Login( user *schema.User, rw http.ResponseWriter, - r *http.Request) (*schema.User, error) { - + r *http.Request, +) (*schema.User, error) { l, err := la.getLdapConnection(false) if err != nil { log.Warn("Error while getting ldap connection") @@ -238,7 +238,6 @@ func (la *LdapAuthenticator) Sync() error { } func (la *LdapAuthenticator) getLdapConnection(admin bool) (*ldap.Conn, error) { - lc := config.Keys.LdapConfig conn, err := ldap.DialURL(lc.Url) if err != nil { diff --git a/internal/auth/oidc.go b/internal/auth/oidc.go index 480b212..cfcf5b6 100644 --- a/internal/auth/oidc.go +++ b/internal/auth/oidc.go @@ -6,38 +6,59 @@ package auth import ( "context" + "crypto/rand" + "encoding/base64" + "io" "log" "net/http" + "strings" + "time" + "github.com/ClusterCockpit/cc-backend/internal/config" "github.com/coreos/go-oidc/v3/oidc" "github.com/gorilla/mux" "golang.org/x/oauth2" ) type OIDC struct { - client *oauth2.Config - provider *oidc.Provider - state string - codeVerifier string + client *oauth2.Config + provider *oidc.Provider +} + +func randString(nByte int) (string, error) { + b := make([]byte, nByte) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +} + +func setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) { + c := &http.Cookie{ + Name: name, + Value: value, + MaxAge: int(time.Hour.Seconds()), + Secure: r.TLS != nil, + HttpOnly: true, + } + http.SetCookie(w, c) } func (oa *OIDC) Init(r *mux.Router) error { - oa.client = &oauth2.Config{ - ClientID: "YOUR_CLIENT_ID", - ClientSecret: "YOUR_CLIENT_SECRET", - Endpoint: oauth2.Endpoint{ - AuthURL: "https://provider.com/o/oauth2/auth", - TokenURL: "https://provider.com/o/oauth2/token", - }, - } - provider, err := oidc.NewProvider(context.Background(), "https://provider") if err != nil { log.Fatal(err) } - oa.provider = provider + oa.client = &oauth2.Config{ + ClientID: "YOUR_CLIENT_ID", + ClientSecret: "YOUR_CLIENT_SECRET", + Endpoint: provider.Endpoint(), + RedirectURL: "https://" + config.Keys.Addr + "/oidc-callback", + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + } + r.HandleFunc("/oidc-login", oa.OAuth2Login) r.HandleFunc("/oidc-callback", oa.OAuth2Callback) @@ -45,9 +66,18 @@ func (oa *OIDC) Init(r *mux.Router) error { } func (oa *OIDC) OAuth2Callback(rw http.ResponseWriter, r *http.Request) { + c, err := r.Cookie("state") + if err != nil { + http.Error(rw, "state not found", http.StatusBadRequest) + return + } + + str := strings.Split(c.Value, " ") + state := str[0] + codeVerifier := str[1] + _ = r.ParseForm() - state := r.Form.Get("state") - if state != oa.state { + if r.Form.Get("state") != state { http.Error(rw, "State invalid", http.StatusBadRequest) return } @@ -56,18 +86,32 @@ func (oa *OIDC) OAuth2Callback(rw http.ResponseWriter, r *http.Request) { http.Error(rw, "Code not found", http.StatusBadRequest) return } - token, err := oa.client.Exchange(context.Background(), code, oauth2.VerifierOption(oa.codeVerifier)) + token, err := oa.client.Exchange(context.Background(), code, oauth2.VerifierOption(codeVerifier)) if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) + http.Error(rw, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError) + return + } + + userInfo, err := oa.provider.UserInfo(context.Background(), oauth2.StaticTokenSource(token)) + if err != nil { + http.Error(rw, "Failed to get userinfo: "+err.Error(), http.StatusInternalServerError) return } } func (oa *OIDC) OAuth2Login(rw http.ResponseWriter, r *http.Request) { + state, err := randString(16) + if err != nil { + http.Error(rw, "Internal error", http.StatusInternalServerError) + return + } + // use PKCE to protect against CSRF attacks - oa.codeVerifier = oauth2.GenerateVerifier() + codeVerifier := oauth2.GenerateVerifier() + + setCallbackCookie(rw, r, "state", strings.Join([]string{state, codeVerifier}, " ")) // Redirect user to consent page to ask for permission - url := oa.client.AuthCodeURL("state", oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(oa.codeVerifier)) + url := oa.client.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(codeVerifier)) http.Redirect(rw, r, url, http.StatusFound) } From e8fb5a00302c54c6930772a27e70b2e75716ccad Mon Sep 17 00:00:00 2001 From: Jan Eitzinger Date: Thu, 28 Mar 2024 12:01:13 +0100 Subject: [PATCH 3/6] Add OpenID Connect authentication Fixes #236 Template conditional not yet working Needs more testing --- cmd/cc-backend/main.go | 12 ++++- internal/auth/auth.go | 40 ++++++++------ internal/auth/oidc.go | 110 ++++++++++++++++++++++++++++++++------- pkg/schema/config.go | 7 ++- pkg/schema/user.go | 1 + web/templates/login.tmpl | 7 +++ 6 files changed, 138 insertions(+), 39 deletions(-) diff --git a/cmd/cc-backend/main.go b/cmd/cc-backend/main.go index 991fe6b..bcbc273 100644 --- a/cmd/cc-backend/main.go +++ b/cmd/cc-backend/main.go @@ -286,6 +286,7 @@ func main() { fmt.Printf("MAIN > JWT for '%s': %s\n", user.Username, jwt) } + } else if flagNewUser != "" || flagDelUser != "" { log.Fatal("arguments --add-user and --del-user can only be used if authentication is enabled") } @@ -343,9 +344,18 @@ func main() { r := mux.NewRouter() buildInfo := web.Build{Version: version, Hash: commit, Buildtime: date} + info := map[string]interface{}{} + info["hasOpenIDConnect"] = "false" + + if config.Keys.OpenIDProvider != "" { + openIDConnect := auth.NewOIDC(authentication) + openIDConnect.RegisterEndpoints(r) + info["hasOpenIDConnect"] = "true" + } + r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") - web.RenderTemplate(rw, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo}) + web.RenderTemplate(rw, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo, Infos: info}) }).Methods(http.MethodGet) r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") diff --git a/internal/auth/auth.go b/internal/auth/auth.go index fe3edad..9bca62e 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -129,6 +129,29 @@ func Init() (*Authentication, error) { return auth, nil } +func (auth *Authentication) SaveSession(rw http.ResponseWriter, r *http.Request, user *schema.User) error { + session, err := auth.sessionStore.New(r, "session") + if err != nil { + log.Errorf("session creation failed: %s", err.Error()) + http.Error(rw, err.Error(), http.StatusInternalServerError) + return err + } + + if auth.SessionMaxAge != 0 { + session.Options.MaxAge = int(auth.SessionMaxAge.Seconds()) + } + session.Values["username"] = user.Username + session.Values["projects"] = user.Projects + session.Values["roles"] = user.Roles + if err := auth.sessionStore.Save(r, rw, session); err != nil { + log.Warnf("session save failed: %s", err.Error()) + http.Error(rw, err.Error(), http.StatusInternalServerError) + return err + } + + return nil +} + func (auth *Authentication) Login( onsuccess http.Handler, onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error), @@ -161,22 +184,7 @@ func (auth *Authentication) Login( return } - session, err := auth.sessionStore.New(r, "session") - if err != nil { - log.Errorf("session creation failed: %s", err.Error()) - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - if auth.SessionMaxAge != 0 { - session.Options.MaxAge = int(auth.SessionMaxAge.Seconds()) - } - session.Values["username"] = user.Username - session.Values["projects"] = user.Projects - session.Values["roles"] = user.Roles - if err := auth.sessionStore.Save(r, rw, session); err != nil { - log.Warnf("session save failed: %s", err.Error()) - http.Error(rw, err.Error(), http.StatusInternalServerError) + if err := auth.SaveSession(rw, r, user); err != nil { return } diff --git a/internal/auth/oidc.go b/internal/auth/oidc.go index cfcf5b6..d29cfde 100644 --- a/internal/auth/oidc.go +++ b/internal/auth/oidc.go @@ -9,20 +9,24 @@ import ( "crypto/rand" "encoding/base64" "io" - "log" "net/http" - "strings" + "os" "time" "github.com/ClusterCockpit/cc-backend/internal/config" + "github.com/ClusterCockpit/cc-backend/internal/repository" + "github.com/ClusterCockpit/cc-backend/pkg/log" + "github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/coreos/go-oidc/v3/oidc" "github.com/gorilla/mux" "golang.org/x/oauth2" ) type OIDC struct { - client *oauth2.Config - provider *oidc.Provider + client *oauth2.Config + provider *oidc.Provider + authentication *Authentication + clientID string } func randString(nByte int) (string, error) { @@ -44,37 +48,48 @@ func setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value strin http.SetCookie(w, c) } -func (oa *OIDC) Init(r *mux.Router) error { - provider, err := oidc.NewProvider(context.Background(), "https://provider") +func NewOIDC(a *Authentication) *OIDC { + provider, err := oidc.NewProvider(context.Background(), config.Keys.OpenIDProvider) if err != nil { log.Fatal(err) } - oa.provider = provider - - oa.client = &oauth2.Config{ - ClientID: "YOUR_CLIENT_ID", - ClientSecret: "YOUR_CLIENT_SECRET", + clientID := os.Getenv("OID_CLIENT_ID") + if clientID == "" { + log.Warn("environment variable 'OID_CLIENT_ID' not set (Open ID connect auth will not work)") + } + clientSecret := os.Getenv("OID_CLIENT_SECRET") + if clientSecret == "" { + log.Warn("environment variable 'OID_CLIENT_SECRET' not set (Open ID connect auth will not work)") + } + redirectURL := "oidc-callback" + client := &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, Endpoint: provider.Endpoint(), - RedirectURL: "https://" + config.Keys.Addr + "/oidc-callback", + RedirectURL: redirectURL, Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } + oa := &OIDC{provider: provider, client: client, clientID: clientID, authentication: a} + + return oa +} + +func (oa *OIDC) RegisterEndpoints(r *mux.Router) { r.HandleFunc("/oidc-login", oa.OAuth2Login) r.HandleFunc("/oidc-callback", oa.OAuth2Callback) - - return nil } func (oa *OIDC) OAuth2Callback(rw http.ResponseWriter, r *http.Request) { c, err := r.Cookie("state") if err != nil { - http.Error(rw, "state not found", http.StatusBadRequest) + http.Error(rw, "state cookie not found", http.StatusBadRequest) return } + state := c.Value - str := strings.Split(c.Value, " ") - state := str[0] - codeVerifier := str[1] + c, err = r.Cookie("verifier") + codeVerifier := c.Value _ = r.ParseForm() if r.Form.Get("state") != state { @@ -97,6 +112,61 @@ func (oa *OIDC) OAuth2Callback(rw http.ResponseWriter, r *http.Request) { http.Error(rw, "Failed to get userinfo: "+err.Error(), http.StatusInternalServerError) return } + + // // Extract the ID Token from OAuth2 token. + // rawIDToken, ok := token.Extra("id_token").(string) + // if !ok { + // http.Error(rw, "Cannot access idToken", http.StatusInternalServerError) + // } + // + // verifier := oa.provider.Verifier(&oidc.Config{ClientID: oa.clientID}) + // // Parse and verify ID Token payload. + // idToken, err := verifier.Verify(context.Background(), rawIDToken) + // if err != nil { + // http.Error(rw, "Failed to extract idToken: "+err.Error(), http.StatusInternalServerError) + // } + + projects := make([]string, 0) + + // Extract custom claims + var claims struct { + Username string `json:"preferred_username"` + Name string `json:"name"` + Profile struct { + Client struct { + Roles []string `json:"roles"` + } `json:"clustercockpit"` + } `json:"resource_access"` + } + if err := userInfo.Claims(&claims); err != nil { + http.Error(rw, "Failed to extract Claims: "+err.Error(), http.StatusInternalServerError) + } + + var roles []string + for _, r := range claims.Profile.Client.Roles { + switch r { + case "user": + roles = append(roles, schema.GetRoleString(schema.RoleUser)) + case "admin": + roles = append(roles, schema.GetRoleString(schema.RoleAdmin)) + } + } + + if len(claims.Profile.Client.Roles) == 0 { + roles = append(roles, schema.GetRoleString(schema.RoleUser)) + } + + user := &schema.User{ + Username: claims.Username, + Name: claims.Name, + Roles: roles, + Projects: projects, + AuthSource: schema.AuthViaOIDC, + } + oa.authentication.SaveSession(rw, r, user) + log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects) + ctx := context.WithValue(r.Context(), repository.ContextUserKey, user) + http.RedirectHandler("/", http.StatusTemporaryRedirect).ServeHTTP(rw, r.WithContext(ctx)) } func (oa *OIDC) OAuth2Login(rw http.ResponseWriter, r *http.Request) { @@ -105,11 +175,11 @@ func (oa *OIDC) OAuth2Login(rw http.ResponseWriter, r *http.Request) { http.Error(rw, "Internal error", http.StatusInternalServerError) return } + setCallbackCookie(rw, r, "state", state) // use PKCE to protect against CSRF attacks codeVerifier := oauth2.GenerateVerifier() - - setCallbackCookie(rw, r, "state", strings.Join([]string{state, codeVerifier}, " ")) + setCallbackCookie(rw, r, "verifier", codeVerifier) // Redirect user to consent page to ask for permission url := oa.client.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(codeVerifier)) diff --git a/pkg/schema/config.go b/pkg/schema/config.go index 5f43fb7..b3b6afb 100644 --- a/pkg/schema/config.go +++ b/pkg/schema/config.go @@ -65,10 +65,10 @@ type ClusterConfig struct { } type Retention struct { - Age int `json:"age"` - IncludeDB bool `json:"includeDB"` Policy string `json:"policy"` Location string `json:"location"` + Age int `json:"age"` + IncludeDB bool `json:"includeDB"` } // Format of the configuration (file). See below for the defaults. @@ -112,6 +112,9 @@ type ProgramConfig struct { LdapConfig *LdapConfig `json:"ldap"` JwtConfig *JWTAuthConfig `json:"jwts"` + // Enable OpenID connect Authentication + OpenIDProvider string `json:"openIDProvider"` + // If 0 or empty, the session does not expire! SessionMaxAge string `json:"session-max-age"` diff --git a/pkg/schema/user.go b/pkg/schema/user.go index 047f617..a227bdb 100644 --- a/pkg/schema/user.go +++ b/pkg/schema/user.go @@ -27,6 +27,7 @@ const ( AuthViaLocalPassword AuthSource = iota AuthViaLDAP AuthViaToken + AuthViaOIDC AuthViaAll ) diff --git a/web/templates/login.tmpl b/web/templates/login.tmpl index 304a96f..47413a6 100644 --- a/web/templates/login.tmpl +++ b/web/templates/login.tmpl @@ -38,7 +38,14 @@ + OpenID Connect Login + {{ range $key, $value := .Infos }} + {{ $key }}: {{ $value }}, + {{ end }} + {{if .Infos.hasOpenIDConnect }} + OpenID Connect Login + {{end}} From c3d250869383bd783f85e18a9e2be18b3e3cf5c0 Mon Sep 17 00:00:00 2001 From: Jan Eitzinger Date: Thu, 28 Mar 2024 12:09:08 +0100 Subject: [PATCH 4/6] Update package deps after merge --- go.mod | 6 +++--- go.sum | 12 ++---------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 2a92f5c..979c1c4 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/99designs/gqlgen v0.17.45 github.com/ClusterCockpit/cc-units v0.4.0 github.com/Masterminds/squirrel v1.5.3 + github.com/coreos/go-oidc/v3 v3.9.0 github.com/go-co-op/gocron v1.25.0 github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-sql-driver/mysql v1.7.0 @@ -27,6 +28,7 @@ require ( github.com/vektah/gqlparser/v2 v2.5.11 golang.org/x/crypto v0.21.0 golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea + golang.org/x/oauth2 v0.13.0 ) require ( @@ -37,7 +39,6 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/containerd/containerd v1.6.18 // indirect - github.com/coreos/go-oidc/v3 v3.9.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/deepmap/oapi-codegen v1.12.4 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect @@ -79,11 +80,10 @@ require ( go.uber.org/atomic v1.10.0 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.22.0 // indirect - golang.org/x/oauth2 v0.5.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.19.0 // indirect - google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index d89d64b..7caeaaa 100644 --- a/go.sum +++ b/go.sum @@ -588,7 +588,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 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= @@ -1428,10 +1427,8 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +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-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1569,8 +1566,6 @@ golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBc 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/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -1734,7 +1729,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= @@ -1860,8 +1854,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -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/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 50401e0030b0a119a7b3311856820012f74b3666 Mon Sep 17 00:00:00 2001 From: Jan Eitzinger Date: Thu, 28 Mar 2024 13:18:25 +0100 Subject: [PATCH 5/6] Fix conditional rendering of OIDC button in login --- cmd/cc-backend/main.go | 9 +++++++-- internal/auth/oidc.go | 4 ++-- web/templates/login.tmpl | 10 +++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/cmd/cc-backend/main.go b/cmd/cc-backend/main.go index bcbc273..e0c18ef 100644 --- a/cmd/cc-backend/main.go +++ b/cmd/cc-backend/main.go @@ -345,16 +345,17 @@ func main() { buildInfo := web.Build{Version: version, Hash: commit, Buildtime: date} info := map[string]interface{}{} - info["hasOpenIDConnect"] = "false" + info["hasOpenIDConnect"] = false if config.Keys.OpenIDProvider != "" { openIDConnect := auth.NewOIDC(authentication) openIDConnect.RegisterEndpoints(r) - info["hasOpenIDConnect"] = "true" + info["hasOpenIDConnect"] = true } r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") + log.Debugf("##%v##", info) web.RenderTemplate(rw, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo, Infos: info}) }).Methods(http.MethodGet) r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) { @@ -382,6 +383,7 @@ func main() { MsgType: "alert-warning", Message: err.Error(), Build: buildInfo, + Infos: info, }) })).Methods(http.MethodPost) @@ -398,6 +400,7 @@ func main() { MsgType: "alert-warning", Message: err.Error(), Build: buildInfo, + Infos: info, }) })) @@ -410,6 +413,7 @@ func main() { MsgType: "alert-info", Message: "Logout successful", Build: buildInfo, + Infos: info, }) }))).Methods(http.MethodPost) @@ -426,6 +430,7 @@ func main() { MsgType: "alert-danger", Message: err.Error(), Build: buildInfo, + Infos: info, }) }) }) diff --git a/internal/auth/oidc.go b/internal/auth/oidc.go index d29cfde..04dcaf3 100644 --- a/internal/auth/oidc.go +++ b/internal/auth/oidc.go @@ -61,12 +61,12 @@ func NewOIDC(a *Authentication) *OIDC { if clientSecret == "" { log.Warn("environment variable 'OID_CLIENT_SECRET' not set (Open ID connect auth will not work)") } - redirectURL := "oidc-callback" + client := &oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, Endpoint: provider.Endpoint(), - RedirectURL: redirectURL, + RedirectURL: "oidc-callback", Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } diff --git a/web/templates/login.tmpl b/web/templates/login.tmpl index 47413a6..f10e064 100644 --- a/web/templates/login.tmpl +++ b/web/templates/login.tmpl @@ -38,14 +38,10 @@ - OpenID Connect Login + {{- if .Infos.hasOpenIDConnect}} + OpenID Connect Login + {{end}} - {{ range $key, $value := .Infos }} - {{ $key }}: {{ $value }}, - {{ end }} - {{if .Infos.hasOpenIDConnect }} - OpenID Connect Login - {{end}} From 6828c97415a2c0b3d3c3565cf0d3ab70949e3c40 Mon Sep 17 00:00:00 2001 From: Jan Eitzinger Date: Thu, 28 Mar 2024 14:22:23 +0100 Subject: [PATCH 6/6] Add central function to persist users on Login --- cmd/cc-backend/main.go | 4 ++-- internal/auth/auth.go | 13 +++++++++++++ internal/auth/jwtCookieSession.go | 4 +--- internal/auth/jwtSession.go | 4 +--- internal/auth/oidc.go | 13 +++++++++++-- pkg/schema/config.go | 13 ++++++++----- 6 files changed, 36 insertions(+), 15 deletions(-) diff --git a/cmd/cc-backend/main.go b/cmd/cc-backend/main.go index e0c18ef..96d7c8d 100644 --- a/cmd/cc-backend/main.go +++ b/cmd/cc-backend/main.go @@ -347,7 +347,7 @@ func main() { info := map[string]interface{}{} info["hasOpenIDConnect"] = false - if config.Keys.OpenIDProvider != "" { + if config.Keys.OpenIDConfig != nil { openIDConnect := auth.NewOIDC(authentication) openIDConnect.RegisterEndpoints(r) info["hasOpenIDConnect"] = true @@ -569,8 +569,8 @@ func main() { } var cfg struct { - Compression int `json:"compression"` Retention schema.Retention `json:"retention"` + Compression int `json:"compression"` } cfg.Retention.IncludeDB = true diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 9bca62e..16e816d 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -129,6 +129,19 @@ func Init() (*Authentication, error) { return auth, nil } +func persistUser(user *schema.User) { + r := repository.GetUserRepository() + _, err := r.GetUser(user.Username) + + if err != nil && err != sql.ErrNoRows { + log.Errorf("Error while loading user '%s': %v", user.Username, err) + } else if err == sql.ErrNoRows { + if err := r.AddUser(user); err != nil { + log.Errorf("Error while adding user '%s' to DB: %v", user.Username, err) + } + } +} + func (auth *Authentication) SaveSession(rw http.ResponseWriter, r *http.Request, user *schema.User) error { session, err := auth.sessionStore.New(r, "session") if err != nil { diff --git a/internal/auth/jwtCookieSession.go b/internal/auth/jwtCookieSession.go index 01f5746..3cf02d9 100644 --- a/internal/auth/jwtCookieSession.go +++ b/internal/auth/jwtCookieSession.go @@ -199,9 +199,7 @@ func (ja *JWTCookieSessionAuthenticator) Login( } if jc.SyncUserOnLogin { - if err := repository.GetUserRepository().AddUser(user); err != nil { - log.Errorf("Error while adding user '%s' to DB", user.Username) - } + persistUser(user) } } diff --git a/internal/auth/jwtSession.go b/internal/auth/jwtSession.go index 541e31e..ca2daf5 100644 --- a/internal/auth/jwtSession.go +++ b/internal/auth/jwtSession.go @@ -139,9 +139,7 @@ func (ja *JWTSessionAuthenticator) Login( } if config.Keys.JwtConfig.SyncUserOnLogin { - if err := repository.GetUserRepository().AddUser(user); err != nil { - log.Errorf("Error while adding user '%s' to DB", user.Username) - } + persistUser(user) } } diff --git a/internal/auth/oidc.go b/internal/auth/oidc.go index 04dcaf3..abfed16 100644 --- a/internal/auth/oidc.go +++ b/internal/auth/oidc.go @@ -49,7 +49,7 @@ func setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value strin } func NewOIDC(a *Authentication) *OIDC { - provider, err := oidc.NewProvider(context.Background(), config.Keys.OpenIDProvider) + provider, err := oidc.NewProvider(context.Background(), config.Keys.OpenIDConfig.Provider) if err != nil { log.Fatal(err) } @@ -89,6 +89,10 @@ func (oa *OIDC) OAuth2Callback(rw http.ResponseWriter, r *http.Request) { state := c.Value c, err = r.Cookie("verifier") + if err != nil { + http.Error(rw, "verifier cookie not found", http.StatusBadRequest) + return + } codeVerifier := c.Value _ = r.ParseForm() @@ -152,7 +156,7 @@ func (oa *OIDC) OAuth2Callback(rw http.ResponseWriter, r *http.Request) { } } - if len(claims.Profile.Client.Roles) == 0 { + if len(roles) == 0 { roles = append(roles, schema.GetRoleString(schema.RoleUser)) } @@ -163,6 +167,11 @@ func (oa *OIDC) OAuth2Callback(rw http.ResponseWriter, r *http.Request) { Projects: projects, AuthSource: schema.AuthViaOIDC, } + + if config.Keys.OpenIDConfig.SyncUserOnLogin { + persistUser(user) + } + oa.authentication.SaveSession(rw, r, user) log.Infof("login successfull: user: %#v (roles: %v, projects: %v)", user.Username, user.Roles, user.Projects) ctx := context.WithValue(r.Context(), repository.ContextUserKey, user) diff --git a/pkg/schema/config.go b/pkg/schema/config.go index b3b6afb..adc47dd 100644 --- a/pkg/schema/config.go +++ b/pkg/schema/config.go @@ -23,6 +23,11 @@ type LdapConfig struct { SyncUserOnLogin bool `json:"syncUserOnLogin"` } +type OpenIDConfig struct { + Provider string `json:"provider"` + SyncUserOnLogin bool `json:"syncUserOnLogin"` +} + type JWTAuthConfig struct { // Specifies for how long a JWT token shall be valid // as a string parsable by time.ParseDuration(). @@ -109,11 +114,9 @@ type ProgramConfig struct { Validate bool `json:"validate"` // For LDAP Authentication and user synchronisation. - LdapConfig *LdapConfig `json:"ldap"` - JwtConfig *JWTAuthConfig `json:"jwts"` - - // Enable OpenID connect Authentication - OpenIDProvider string `json:"openIDProvider"` + LdapConfig *LdapConfig `json:"ldap"` + JwtConfig *JWTAuthConfig `json:"jwts"` + OpenIDConfig *OpenIDConfig `json:"oidc"` // If 0 or empty, the session does not expire! SessionMaxAge string `json:"session-max-age"`