mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-11-04 01:25:06 +01:00 
			
		
		
		
	feat: Add OpenID Connect Authentication support
236 user authentication using keycloak or any openid client for using external auth providers such as ldap GitHub google
This commit is contained in:
		@@ -286,6 +286,7 @@ func main() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			fmt.Printf("MAIN > JWT for '%s': %s\n", user.Username, jwt)
 | 
								fmt.Printf("MAIN > JWT for '%s': %s\n", user.Username, jwt)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	} else if flagNewUser != "" || flagDelUser != "" {
 | 
						} else if flagNewUser != "" || flagDelUser != "" {
 | 
				
			||||||
		log.Fatal("arguments --add-user and --del-user can only be used if authentication is enabled")
 | 
							log.Fatal("arguments --add-user and --del-user can only be used if authentication is enabled")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -343,9 +344,19 @@ func main() {
 | 
				
			|||||||
	r := mux.NewRouter()
 | 
						r := mux.NewRouter()
 | 
				
			||||||
	buildInfo := web.Build{Version: version, Hash: commit, Buildtime: date}
 | 
						buildInfo := web.Build{Version: version, Hash: commit, Buildtime: date}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						info := map[string]interface{}{}
 | 
				
			||||||
 | 
						info["hasOpenIDConnect"] = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if config.Keys.OpenIDConfig != nil {
 | 
				
			||||||
 | 
							openIDConnect := auth.NewOIDC(authentication)
 | 
				
			||||||
 | 
							openIDConnect.RegisterEndpoints(r)
 | 
				
			||||||
 | 
							info["hasOpenIDConnect"] = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) {
 | 
						r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
							rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
				
			||||||
		web.RenderTemplate(rw, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo})
 | 
							log.Debugf("##%v##", info)
 | 
				
			||||||
 | 
							web.RenderTemplate(rw, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo, Infos: info})
 | 
				
			||||||
	}).Methods(http.MethodGet)
 | 
						}).Methods(http.MethodGet)
 | 
				
			||||||
	r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) {
 | 
						r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
							rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
				
			||||||
@@ -372,6 +383,7 @@ func main() {
 | 
				
			|||||||
					MsgType: "alert-warning",
 | 
										MsgType: "alert-warning",
 | 
				
			||||||
					Message: err.Error(),
 | 
										Message: err.Error(),
 | 
				
			||||||
					Build:   buildInfo,
 | 
										Build:   buildInfo,
 | 
				
			||||||
 | 
										Infos:   info,
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
			})).Methods(http.MethodPost)
 | 
								})).Methods(http.MethodPost)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -388,6 +400,7 @@ func main() {
 | 
				
			|||||||
					MsgType: "alert-warning",
 | 
										MsgType: "alert-warning",
 | 
				
			||||||
					Message: err.Error(),
 | 
										Message: err.Error(),
 | 
				
			||||||
					Build:   buildInfo,
 | 
										Build:   buildInfo,
 | 
				
			||||||
 | 
										Infos:   info,
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
			}))
 | 
								}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -400,6 +413,7 @@ func main() {
 | 
				
			|||||||
					MsgType: "alert-info",
 | 
										MsgType: "alert-info",
 | 
				
			||||||
					Message: "Logout successful",
 | 
										Message: "Logout successful",
 | 
				
			||||||
					Build:   buildInfo,
 | 
										Build:   buildInfo,
 | 
				
			||||||
 | 
										Infos:   info,
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
			}))).Methods(http.MethodPost)
 | 
								}))).Methods(http.MethodPost)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -416,6 +430,7 @@ func main() {
 | 
				
			|||||||
						MsgType: "alert-danger",
 | 
											MsgType: "alert-danger",
 | 
				
			||||||
						Message: err.Error(),
 | 
											Message: err.Error(),
 | 
				
			||||||
						Build:   buildInfo,
 | 
											Build:   buildInfo,
 | 
				
			||||||
 | 
											Infos:   info,
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
@@ -554,8 +569,8 @@ func main() {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var cfg struct {
 | 
						var cfg struct {
 | 
				
			||||||
		Compression int              `json:"compression"`
 | 
					 | 
				
			||||||
		Retention   schema.Retention `json:"retention"`
 | 
							Retention   schema.Retention `json:"retention"`
 | 
				
			||||||
 | 
							Compression int              `json:"compression"`
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	cfg.Retention.IncludeDB = true
 | 
						cfg.Retention.IncludeDB = true
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								go.mod
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										8
									
								
								go.mod
									
									
									
									
									
								
							@@ -6,6 +6,7 @@ require (
 | 
				
			|||||||
	github.com/99designs/gqlgen v0.17.45
 | 
						github.com/99designs/gqlgen v0.17.45
 | 
				
			||||||
	github.com/ClusterCockpit/cc-units v0.4.0
 | 
						github.com/ClusterCockpit/cc-units v0.4.0
 | 
				
			||||||
	github.com/Masterminds/squirrel v1.5.3
 | 
						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-co-op/gocron v1.25.0
 | 
				
			||||||
	github.com/go-ldap/ldap/v3 v3.4.4
 | 
						github.com/go-ldap/ldap/v3 v3.4.4
 | 
				
			||||||
	github.com/go-sql-driver/mysql v1.7.0
 | 
						github.com/go-sql-driver/mysql v1.7.0
 | 
				
			||||||
@@ -27,6 +28,7 @@ require (
 | 
				
			|||||||
	github.com/vektah/gqlparser/v2 v2.5.11
 | 
						github.com/vektah/gqlparser/v2 v2.5.11
 | 
				
			||||||
	golang.org/x/crypto v0.21.0
 | 
						golang.org/x/crypto v0.21.0
 | 
				
			||||||
	golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea
 | 
						golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea
 | 
				
			||||||
 | 
						golang.org/x/oauth2 v0.13.0
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
@@ -41,11 +43,12 @@ require (
 | 
				
			|||||||
	github.com/deepmap/oapi-codegen v1.12.4 // indirect
 | 
						github.com/deepmap/oapi-codegen v1.12.4 // indirect
 | 
				
			||||||
	github.com/felixge/httpsnoop v1.0.3 // indirect
 | 
						github.com/felixge/httpsnoop v1.0.3 // indirect
 | 
				
			||||||
	github.com/go-asn1-ber/asn1-ber v1.5.4 // 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.21.0 // indirect
 | 
						github.com/go-openapi/jsonpointer v0.21.0 // indirect
 | 
				
			||||||
	github.com/go-openapi/jsonreference v0.21.0 // indirect
 | 
						github.com/go-openapi/jsonreference v0.21.0 // indirect
 | 
				
			||||||
	github.com/go-openapi/spec v0.21.0 // indirect
 | 
						github.com/go-openapi/spec v0.21.0 // indirect
 | 
				
			||||||
	github.com/go-openapi/swag v0.23.0 // indirect
 | 
						github.com/go-openapi/swag v0.23.0 // indirect
 | 
				
			||||||
	github.com/golang/protobuf v1.5.2 // indirect
 | 
						github.com/golang/protobuf v1.5.3 // indirect
 | 
				
			||||||
	github.com/google/uuid v1.6.0 // indirect
 | 
						github.com/google/uuid v1.6.0 // indirect
 | 
				
			||||||
	github.com/gorilla/securecookie v1.1.1 // indirect
 | 
						github.com/gorilla/securecookie v1.1.1 // indirect
 | 
				
			||||||
	github.com/gorilla/websocket v1.5.0 // indirect
 | 
						github.com/gorilla/websocket v1.5.0 // indirect
 | 
				
			||||||
@@ -77,11 +80,10 @@ require (
 | 
				
			|||||||
	go.uber.org/atomic v1.10.0 // indirect
 | 
						go.uber.org/atomic v1.10.0 // indirect
 | 
				
			||||||
	golang.org/x/mod v0.16.0 // indirect
 | 
						golang.org/x/mod v0.16.0 // indirect
 | 
				
			||||||
	golang.org/x/net v0.22.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/sys v0.18.0 // indirect
 | 
				
			||||||
	golang.org/x/text v0.14.0 // indirect
 | 
						golang.org/x/text v0.14.0 // indirect
 | 
				
			||||||
	golang.org/x/tools v0.19.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
 | 
						google.golang.org/protobuf v1.33.0 // indirect
 | 
				
			||||||
	gopkg.in/yaml.v2 v2.4.0 // indirect
 | 
						gopkg.in/yaml.v2 v2.4.0 // indirect
 | 
				
			||||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
						gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								go.sum
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										15
									
								
								go.sum
									
									
									
									
									
								
							@@ -341,6 +341,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.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
 | 
				
			||||||
github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
 | 
					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 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.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-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=
 | 
					github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 | 
				
			||||||
@@ -453,6 +455,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-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-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-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.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 | 
				
			||||||
github.com/go-kit/kit v0.9.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=
 | 
					github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
 | 
				
			||||||
@@ -584,8 +588,9 @@ 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.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 | 
				
			||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 | 
					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.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.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.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.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
				
			||||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
					github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
				
			||||||
@@ -1422,8 +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-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-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.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 | 
				
			||||||
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
 | 
					golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
 | 
				
			||||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
 | 
					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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
					golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
@@ -1578,6 +1583,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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
				
			||||||
golang.org/x/text v0.3.6/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.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.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 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 | 
				
			||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 | 
					golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 | 
				
			||||||
@@ -1723,8 +1729,9 @@ 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.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 | 
				
			||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 | 
					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.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.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/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-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 | 
				
			||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 | 
					google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,17 +28,17 @@ type Authenticator interface {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type Authentication struct {
 | 
					type Authentication struct {
 | 
				
			||||||
	sessionStore   *sessions.CookieStore
 | 
						sessionStore   *sessions.CookieStore
 | 
				
			||||||
	SessionMaxAge time.Duration
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	authenticators []Authenticator
 | 
					 | 
				
			||||||
	LdapAuth       *LdapAuthenticator
 | 
						LdapAuth       *LdapAuthenticator
 | 
				
			||||||
	JwtAuth        *JWTAuthenticator
 | 
						JwtAuth        *JWTAuthenticator
 | 
				
			||||||
	LocalAuth      *LocalAuthenticator
 | 
						LocalAuth      *LocalAuthenticator
 | 
				
			||||||
 | 
						authenticators []Authenticator
 | 
				
			||||||
 | 
						SessionMaxAge  time.Duration
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (auth *Authentication) AuthViaSession(
 | 
					func (auth *Authentication) AuthViaSession(
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) (*schema.User, error) {
 | 
						r *http.Request,
 | 
				
			||||||
 | 
					) (*schema.User, error) {
 | 
				
			||||||
	session, err := auth.sessionStore.Get(r, "session")
 | 
						session, err := auth.sessionStore.Get(r, "session")
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Error("Error while getting session store")
 | 
							log.Error("Error while getting session store")
 | 
				
			||||||
@@ -129,10 +129,46 @@ func Init() (*Authentication, error) {
 | 
				
			|||||||
	return auth, nil
 | 
						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 {
 | 
				
			||||||
 | 
							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(
 | 
					func (auth *Authentication) Login(
 | 
				
			||||||
	onsuccess http.Handler,
 | 
						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) {
 | 
						return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		username := r.FormValue("username")
 | 
							username := r.FormValue("username")
 | 
				
			||||||
		var dbUser *schema.User
 | 
							var dbUser *schema.User
 | 
				
			||||||
@@ -161,22 +197,7 @@ func (auth *Authentication) Login(
 | 
				
			|||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			session, err := auth.sessionStore.New(r, "session")
 | 
								if err := auth.SaveSession(rw, r, user); err != nil {
 | 
				
			||||||
			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)
 | 
					 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -193,10 +214,9 @@ func (auth *Authentication) Login(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (auth *Authentication) Auth(
 | 
					func (auth *Authentication) Auth(
 | 
				
			||||||
	onsuccess http.Handler,
 | 
						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) {
 | 
						return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
		user, err := auth.JwtAuth.AuthViaJWT(rw, r)
 | 
							user, err := auth.JwtAuth.AuthViaJWT(rw, r)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Infof("authentication failed: %s", err.Error())
 | 
								log.Infof("authentication failed: %s", err.Error())
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -199,9 +199,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if jc.SyncUserOnLogin {
 | 
							if jc.SyncUserOnLogin {
 | 
				
			||||||
			if err := repository.GetUserRepository().AddUser(user); err != nil {
 | 
								persistUser(user)
 | 
				
			||||||
				log.Errorf("Error while adding user '%s' to DB", user.Username)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -139,9 +139,7 @@ func (ja *JWTSessionAuthenticator) Login(
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if config.Keys.JwtConfig.SyncUserOnLogin {
 | 
							if config.Keys.JwtConfig.SyncUserOnLogin {
 | 
				
			||||||
			if err := repository.GetUserRepository().AddUser(user); err != nil {
 | 
								persistUser(user)
 | 
				
			||||||
				log.Errorf("Error while adding user '%s' to DB", user.Username)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -74,8 +74,8 @@ func (la *LdapAuthenticator) CanLogin(
 | 
				
			|||||||
	user *schema.User,
 | 
						user *schema.User,
 | 
				
			||||||
	username string,
 | 
						username string,
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) (*schema.User, bool) {
 | 
						r *http.Request,
 | 
				
			||||||
 | 
					) (*schema.User, bool) {
 | 
				
			||||||
	lc := config.Keys.LdapConfig
 | 
						lc := config.Keys.LdapConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if user != nil {
 | 
						if user != nil {
 | 
				
			||||||
@@ -138,8 +138,8 @@ func (la *LdapAuthenticator) CanLogin(
 | 
				
			|||||||
func (la *LdapAuthenticator) Login(
 | 
					func (la *LdapAuthenticator) Login(
 | 
				
			||||||
	user *schema.User,
 | 
						user *schema.User,
 | 
				
			||||||
	rw http.ResponseWriter,
 | 
						rw http.ResponseWriter,
 | 
				
			||||||
	r *http.Request) (*schema.User, error) {
 | 
						r *http.Request,
 | 
				
			||||||
 | 
					) (*schema.User, error) {
 | 
				
			||||||
	l, err := la.getLdapConnection(false)
 | 
						l, err := la.getLdapConnection(false)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Warn("Error while getting ldap connection")
 | 
							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) {
 | 
					func (la *LdapAuthenticator) getLdapConnection(admin bool) (*ldap.Conn, error) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
	lc := config.Keys.LdapConfig
 | 
						lc := config.Keys.LdapConfig
 | 
				
			||||||
	conn, err := ldap.DialURL(lc.Url)
 | 
						conn, err := ldap.DialURL(lc.Url)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										196
									
								
								internal/auth/oidc.go
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										196
									
								
								internal/auth/oidc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,196 @@
 | 
				
			|||||||
 | 
					// 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"
 | 
				
			||||||
 | 
						"crypto/rand"
 | 
				
			||||||
 | 
						"encoding/base64"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"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
 | 
				
			||||||
 | 
						authentication *Authentication
 | 
				
			||||||
 | 
						clientID       string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 NewOIDC(a *Authentication) *OIDC {
 | 
				
			||||||
 | 
						provider, err := oidc.NewProvider(context.Background(), config.Keys.OpenIDConfig.Provider)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						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)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						client := &oauth2.Config{
 | 
				
			||||||
 | 
							ClientID:     clientID,
 | 
				
			||||||
 | 
							ClientSecret: clientSecret,
 | 
				
			||||||
 | 
							Endpoint:     provider.Endpoint(),
 | 
				
			||||||
 | 
							RedirectURL:  "oidc-callback",
 | 
				
			||||||
 | 
							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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (oa *OIDC) OAuth2Callback(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						c, err := r.Cookie("state")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							http.Error(rw, "state cookie not found", http.StatusBadRequest)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						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()
 | 
				
			||||||
 | 
						if r.Form.Get("state") != 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(codeVerifier))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							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
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// // 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(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,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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)
 | 
				
			||||||
 | 
						http.RedirectHandler("/", http.StatusTemporaryRedirect).ServeHTTP(rw, r.WithContext(ctx))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						setCallbackCookie(rw, r, "state", state)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// use PKCE to protect against CSRF attacks
 | 
				
			||||||
 | 
						codeVerifier := oauth2.GenerateVerifier()
 | 
				
			||||||
 | 
						setCallbackCookie(rw, r, "verifier", codeVerifier)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Redirect user to consent page to ask for permission
 | 
				
			||||||
 | 
						url := oa.client.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(codeVerifier))
 | 
				
			||||||
 | 
						http.Redirect(rw, r, url, http.StatusFound)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -23,6 +23,11 @@ type LdapConfig struct {
 | 
				
			|||||||
	SyncUserOnLogin bool `json:"syncUserOnLogin"`
 | 
						SyncUserOnLogin bool `json:"syncUserOnLogin"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type OpenIDConfig struct {
 | 
				
			||||||
 | 
						Provider        string `json:"provider"`
 | 
				
			||||||
 | 
						SyncUserOnLogin bool   `json:"syncUserOnLogin"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type JWTAuthConfig struct {
 | 
					type JWTAuthConfig struct {
 | 
				
			||||||
	// Specifies for how long a JWT token shall be valid
 | 
						// Specifies for how long a JWT token shall be valid
 | 
				
			||||||
	// as a string parsable by time.ParseDuration().
 | 
						// as a string parsable by time.ParseDuration().
 | 
				
			||||||
@@ -65,10 +70,10 @@ type ClusterConfig struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Retention struct {
 | 
					type Retention struct {
 | 
				
			||||||
	Age       int    `json:"age"`
 | 
					 | 
				
			||||||
	IncludeDB bool   `json:"includeDB"`
 | 
					 | 
				
			||||||
	Policy    string `json:"policy"`
 | 
						Policy    string `json:"policy"`
 | 
				
			||||||
	Location  string `json:"location"`
 | 
						Location  string `json:"location"`
 | 
				
			||||||
 | 
						Age       int    `json:"age"`
 | 
				
			||||||
 | 
						IncludeDB bool   `json:"includeDB"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Format of the configuration (file). See below for the defaults.
 | 
					// Format of the configuration (file). See below for the defaults.
 | 
				
			||||||
@@ -111,6 +116,7 @@ type ProgramConfig struct {
 | 
				
			|||||||
	// For LDAP Authentication and user synchronisation.
 | 
						// For LDAP Authentication and user synchronisation.
 | 
				
			||||||
	LdapConfig   *LdapConfig    `json:"ldap"`
 | 
						LdapConfig   *LdapConfig    `json:"ldap"`
 | 
				
			||||||
	JwtConfig    *JWTAuthConfig `json:"jwts"`
 | 
						JwtConfig    *JWTAuthConfig `json:"jwts"`
 | 
				
			||||||
 | 
						OpenIDConfig *OpenIDConfig  `json:"oidc"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// If 0 or empty, the session does not expire!
 | 
						// If 0 or empty, the session does not expire!
 | 
				
			||||||
	SessionMaxAge string `json:"session-max-age"`
 | 
						SessionMaxAge string `json:"session-max-age"`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,6 +27,7 @@ const (
 | 
				
			|||||||
	AuthViaLocalPassword AuthSource = iota
 | 
						AuthViaLocalPassword AuthSource = iota
 | 
				
			||||||
	AuthViaLDAP
 | 
						AuthViaLDAP
 | 
				
			||||||
	AuthViaToken
 | 
						AuthViaToken
 | 
				
			||||||
 | 
						AuthViaOIDC
 | 
				
			||||||
	AuthViaAll
 | 
						AuthViaAll
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,6 +38,9 @@
 | 
				
			|||||||
                                    <input class="form-control" type="password" id="password" name="password" required/>
 | 
					                                    <input class="form-control" type="password" id="password" name="password" required/>
 | 
				
			||||||
                                </div>
 | 
					                                </div>
 | 
				
			||||||
                                <button type="submit" class="btn btn-success">Submit</button>
 | 
					                                <button type="submit" class="btn btn-success">Submit</button>
 | 
				
			||||||
 | 
					                                {{- if .Infos.hasOpenIDConnect}}
 | 
				
			||||||
 | 
					                                <a class="btn btn-primary" href="/oidc-login">OpenID Connect Login</a>
 | 
				
			||||||
 | 
					                                {{end}}
 | 
				
			||||||
                            </form>
 | 
					                            </form>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user