├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── alice-example ├── LICENSE ├── README.md └── main.go ├── go.mod ├── go.sum ├── gorilla-example ├── LICENSE ├── README.md └── main.go └── openid ├── LICENSE ├── README.md ├── configuration.go ├── configurationgettermock_test.go ├── configurationprovider.go ├── configurationprovider_test.go ├── doc.go ├── errors.go ├── example_test.go ├── httpclientmock_test.go ├── idtokenvalidator.go ├── idtokenvalidator_test.go ├── integration_test.go ├── jwksgettermock_test.go ├── jwksprovider.go ├── jwksprovider_test.go ├── jwtparsermock_test.go ├── jwttokenvalidatormock_test.go ├── middleware.go ├── middleware_test.go ├── pemencodemock_test.go ├── provider.go ├── provider_test.go ├── providersgettermock_test.go ├── readidtoken.go ├── readidtoken_test.go ├── signingkeyencoder.go ├── signingkeyencoder_test.go ├── signingkeygettermock_test.go ├── signingkeyprovider.go ├── signingkeyprovider_test.go ├── signingkeysetgettermock_test.go ├── signingkeysetprovider.go ├── signingkeysetprovider_test.go ├── user.go └── userhandler.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.go~ 2 | *.exe 3 | *.go# 4 | 5 | .idea/ 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [0.2.0] - 2022-05-14 9 | 10 | Update with `go get https://github.com/TykTechnologies/openid2go`. 11 | 12 | ### Added 13 | 14 | - Bump version of [golang-jwt]() to v4.4.1 and fix library change 15 | 16 | ## [0.1.0] - 2021-10-09 17 | 18 | Update with `go get https://github.com/TykTechnologies/openid2go`. 19 | 20 | ### Fixed 21 | 22 | - build: update to account for dependency moving orgs 23 | - build: updated to resolve [CVE-2020-26160](https://github.com/advisories/GHSA-w73w-5m7g-f7qc) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Emanoel Xavier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Go OpenId 2 | =========== 3 | 4 | [![Join the chat at https://gitter.im/emanoelxavier/openid2go](https://badges.gitter.im/emanoelxavier/openid2go.svg)](https://gitter.im/emanoelxavier/openid2go?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/emanoelxavier/openid2go/openid) 6 | [![license](http://img.shields.io/badge/license-MIT-yellowgreen.svg?style=flat)](https://raw.githubusercontent.com/emanoelxavier/openid2go/master/openid/LICENSE) 7 | ## Summary 8 | 9 | A Go package that implements web service middlewares for authenticating identities represented by OpenID Connect (OIDC) ID Tokens. 10 | 11 | "OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. It enables Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server" - [OpenID Connect](http://openid.net/specs/openid-connect-core-1_0.html) 12 | 13 | ## Installation 14 | 15 | ```sh 16 | go get github.com/emanoelxavier/openid2go/openid 17 | ``` 18 | 19 | ## Example 20 | This example demonstrates how to use this package to validate incoming ID Tokens. It initializes the Configuration with the desired providers (OPs) and registers two middlewares: openid.Authenticate and openid.AuthenticateUser. The former performs the token validation while the latter, in addition to that, will forward the user information to the next handler. 21 | 22 | ```go 23 | import ( 24 | "fmt" 25 | "net/http" 26 | 27 | "github.com/emanoelxavier/openid2go/openid" 28 | ) 29 | 30 | func AuthenticatedHandler(w http.ResponseWriter, r *http.Request) { 31 | fmt.Fprintln(w, "The user was authenticated!") 32 | } 33 | 34 | func AuthenticatedHandlerWithUser(u *openid.User, w http.ResponseWriter, r *http.Request) { 35 | fmt.Fprintf(w, "The user was authenticated! The token was issued by %v and the user is %+v.", u.Issuer, u) 36 | } 37 | 38 | func Example() { 39 | configuration, err := openid.NewConfiguration(openid.ProvidersGetter(getProviders_googlePlayground)) 40 | 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | http.Handle("/user", openid.AuthenticateUser(configuration, openid.UserHandlerFunc(AuthenticatedHandlerWithUser))) 46 | http.Handle("/authn", openid.Authenticate(configuration, http.HandlerFunc(AuthenticatedHandler))) 47 | 48 | http.ListenAndServe(":5100", nil) 49 | } 50 | 51 | func myGetProviders() ([]openid.Provider, error) { 52 | provider, err := openid.NewProvider("https://providerissuer", []string{"myClientID"}) 53 | 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return []openid.Provider{provider}, nil 59 | } 60 | ``` 61 | This example is also available in the documentation of this package, for more details see [GoDoc](https://godoc.org/github.com/emanoelxavier/openid2go/openid). 62 | 63 | An additional example using [Alice](https://github.com/justinas/alice) can be found at [Alice Example](https://github.com/emanoelxavier/openid2go/tree/master/alice-example) 64 | 65 | ## Tests 66 | 67 | #### Unit Tests 68 | 69 | ```sh 70 | go test github.com/emanoelxavier/openid2go/openid 71 | ``` 72 | 73 | #### Integration Tests 74 | In addition to to unit tests, this package also comes with integration tests that will validate real ID Tokens issued by real OIDC providers. The following command will run those tests: 75 | 76 | 77 | ```sh 78 | go test -tags integration github.com/emanoelxavier/openid2go/openid -issuer=[issuer] -clientID=[clientID] -idToken=[idToken] 79 | ``` 80 | 81 | Replace [issuer], [clientID] and [idToken] with the information from an identity provider of your choice. 82 | 83 | For a quick spin you can use it with tokens issued by Google for the [Google OAuth PlayGround](https://developers.google.com/oauthplayground) entering "openid" (without quotes) within the scope field and copying the issued ID Token. For this provider and client the values will be: 84 | 85 | 86 | ```sh 87 | go test -tags integration github.com/emanoelxavier/openid2go/openid -issuer=https://accounts.google.com -clientID=407408718192.apps.googleusercontent.com -idToken=copiedIDToken 88 | ``` 89 | 90 | ## Contributing 91 | 92 | 1. Open an issue if found a bug or have a functional request. 93 | 2. Disccuss. 94 | 3. Branch off, write the fix with test(s) and commit attaching to the issue. 95 | 4. Make a pull request. -------------------------------------------------------------------------------- /alice-example/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Emanoel Xavier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /alice-example/README.md: -------------------------------------------------------------------------------- 1 | Go OpenId - Alice Example 2 | =========== 3 | [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/emanoelxavier/openid2go/openid) 4 | [![license](http://img.shields.io/badge/license-MIT-yellowgreen.svg?style=flat)](https://raw.githubusercontent.com/emanoelxavier/openid2go/master/alice-example/LICENSE) 5 | 6 | This fully working example implements an HTTP server chaining openid Authentication middlewares with various other middlewares using [Alice](https://github.com/justinas/alice). 7 | 8 | Alice allows easily chaining middlewares in the form: 9 | 10 | ```go 11 | func (http.Handler) http.Handler 12 | ``` 13 | 14 | However the Authentication middlewares exported by the package openid2go/openid have slightly different constructors: 15 | 16 | ```go 17 | func Authenticate(conf *Configuration, h http.Handler) http.Handler 18 | ``` 19 | ```go 20 | func AuthenticateUser(conf *Configuration, h UserHandler) http.Handler 21 | ``` 22 | 23 | This example demonstrates that those middlewares can still be easily chained using Alice with little additional code. 24 | 25 | ## Test 26 | 27 | Download and build: 28 | ```sh 29 | go get github.com/emanoelxavier/openid2go/alice-example 30 | ``` 31 | ```sh 32 | go build github.com/emanoelxavier/openid2go/alice-example 33 | ``` 34 | 35 | Run: 36 | ```sh 37 | github.com\emanoelxavier\openid2go\alice-example\alice-example.exe 38 | ``` 39 | 40 | Once running you can send requests like the ones below: 41 | ```sh 42 | GET http://localhost:5103 43 | ``` 44 | ```sh 45 | GET http://localhost:5103/me 46 | Authorization: Bearer eyJhbGciOiJS... 47 | ```` 48 | ```sh 49 | GET http://localhost:5103/authn 50 | Authorization: Bearer eyJhbGciOiJS... 51 | ``` 52 | The abbreviated token above must be replaced with the IDToken acquired from the [Google OAuth PlayGround](https://developers.google.com/oauthplayground) entering "openid" (without quotes) within the scope field. -------------------------------------------------------------------------------- /alice-example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/TykTechnologies/openid2go/openid" 9 | "github.com/gorilla/context" 10 | "github.com/justinas/alice" 11 | ) 12 | 13 | const UserKey = 0 14 | 15 | func authenticatedHandler(w http.ResponseWriter, r *http.Request) { 16 | fmt.Fprintln(w, "The user was authenticated successfully!") 17 | } 18 | 19 | func unauthenticatedHandler(w http.ResponseWriter, r *http.Request) { 20 | fmt.Fprintln(w, "Reached without authentication!") 21 | } 22 | 23 | func meHandler(w http.ResponseWriter, r *http.Request) { 24 | u := context.Get(r, UserKey).(*openid.User) 25 | fmt.Fprintf(w, "Hello %v! this is all I know about you: %+v.", u.ID, u) 26 | } 27 | 28 | func timeoutMiddleware(h http.Handler) http.Handler { 29 | return http.TimeoutHandler(h, 1*time.Second, "timed out") 30 | } 31 | 32 | func myMiddleware(h http.Handler) http.Handler { 33 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 | h.ServeHTTP(w, r) 35 | fmt.Fprint(w, "Executed my middleware!") 36 | }) 37 | } 38 | 39 | var provider *openid.Provider 40 | var configuration *openid.Configuration 41 | 42 | type userHandlerAdapter struct { 43 | h http.Handler 44 | } 45 | 46 | func (a *userHandlerAdapter) ServeHTTPWithUser(u *openid.User, rw http.ResponseWriter, req *http.Request) { 47 | context.Set(req, UserKey, u) 48 | a.h.ServeHTTP(rw, req) 49 | } 50 | 51 | func (a *userHandlerAdapter) myAuthenticateUser(h http.Handler) http.Handler { 52 | a.h = h 53 | return openid.AuthenticateUser(configuration, a) 54 | } 55 | 56 | func myAuthenticate(h http.Handler) http.Handler { 57 | return openid.Authenticate(configuration, h) 58 | } 59 | 60 | func main() { 61 | configuration, _ = openid.NewConfiguration(openid.ProvidersGetter(getProviders_googlePlayground)) 62 | 63 | adapter := new(userHandlerAdapter) 64 | 65 | http.Handle("/me", alice.New(timeoutMiddleware, myMiddleware, adapter.myAuthenticateUser).ThenFunc(meHandler)) 66 | http.Handle("/authn", alice.New(timeoutMiddleware, myMiddleware, myAuthenticate).ThenFunc(authenticatedHandler)) 67 | http.HandleFunc("/", unauthenticatedHandler) 68 | 69 | http.ListenAndServe(":5103", nil) 70 | } 71 | 72 | // getProviders returns the identity providers that will authenticate the users of the underlying service. 73 | // A Provider is composed by its unique issuer and the collection of client IDs registered with the provider that 74 | // are allowed to call this service. 75 | // On this example Google OP is the provider of choice and the client ID used corresponds 76 | // to the Google OAUTH Playground https://developers.google.com/oauthplayground 77 | func getProviders_googlePlayground() ([]openid.Provider, error) { 78 | provider, err := openid.NewProvider("https://accounts.google.com", []string{"407408718192.apps.googleusercontent.com"}) 79 | 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return []openid.Provider{provider}, nil 85 | } 86 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/TykTechnologies/openid2go 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-jose/go-jose/v3 v3.0.0 7 | github.com/golang-jwt/jwt/v4 v4.4.2 8 | github.com/google/go-cmp v0.5.6 // indirect 9 | github.com/gorilla/context v1.1.1 10 | github.com/justinas/alice v1.2.0 11 | github.com/stretchr/testify v1.7.0 // indirect 12 | golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= 4 | github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= 5 | github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= 6 | github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 7 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 8 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 9 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 10 | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 11 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 12 | github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= 13 | github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 18 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 19 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 20 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 21 | golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 22 | golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c= 23 | golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 24 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 25 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 26 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 27 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 28 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 30 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 32 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 33 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 34 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 35 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 37 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 38 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 39 | -------------------------------------------------------------------------------- /gorilla-example/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Emanoel Xavier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /gorilla-example/README.md: -------------------------------------------------------------------------------- 1 | Go OpenId - Gorilla Example 2 | =========== 3 | [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/emanoelxavier/openid2go/openid) 4 | [![license](http://img.shields.io/badge/license-MIT-yellowgreen.svg?style=flat)](https://raw.githubusercontent.com/emanoelxavier/openid2go/master/gorilla-example/LICENSE) 5 | 6 | This fully working example implements an HTTP server using openid Authentication middlewares and [Gorilla Context](http://www.gorillatoolkit.org/pkg/context) to preserve the user information accross the service application stack. 7 | 8 | 9 | The AuthenticateUser middleware exported by the package openid2go/openid forwards the user information to a handler that implements the interface openid.UserHandler: 10 | 11 | 12 | ```go 13 | func AuthenticateUser(conf *Configuration, h UserHandler) http.Handler 14 | ``` 15 | 16 | ```go 17 | type UserHandler interface { 18 | ServeHTTPWithUser(*User, http.ResponseWriter, *http.Request) 19 | } 20 | ``` 21 | 22 | This example demonstrates how to create an adapter that implements that interface and use it to store the openid.User into a Gorilla Context. The user information can then be retrieved from the context in another point of the application stack. 23 | 24 | ## Test 25 | 26 | Download and build: 27 | ```sh 28 | go get github.com/emanoelxavier/openid2go/gorilla-example 29 | ``` 30 | ```sh 31 | go build github.com/emanoelxavier/openid2go/gorilla-example 32 | ``` 33 | 34 | Run: 35 | ```sh 36 | github.com\emanoelxavier\openid2go\alice-example\gorilla-example.exe 37 | ``` 38 | 39 | Once running you can send requests like the ones below: 40 | ```sh 41 | GET http://localhost:5100 42 | ``` 43 | ```sh 44 | GET http://localhost:5100/me 45 | Authorization: Bearer eyJhbGciOiJS... 46 | ```` 47 | ```sh 48 | GET http://localhost:5100/authn 49 | Authorization: Bearer eyJhbGciOiJS... 50 | ``` 51 | The abbreviated token above must be replaced with the IDToken acquired from the [Google OAuth PlayGround](https://developers.google.com/oauthplayground) entering "openid" (without quotes) within the scope field. 52 | -------------------------------------------------------------------------------- /gorilla-example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/TykTechnologies/openid2go/openid" 8 | "github.com/gorilla/context" 9 | ) 10 | 11 | const UserKey = 0 12 | 13 | func authenticatedHandler(w http.ResponseWriter, r *http.Request) { 14 | fmt.Fprintln(w, "The user was authenticated successfully!") 15 | } 16 | 17 | func unauthenticatedHandler(w http.ResponseWriter, r *http.Request) { 18 | fmt.Fprintln(w, "Reached without authentication!") 19 | } 20 | 21 | func meHandler(w http.ResponseWriter, r *http.Request) { 22 | u := context.Get(r, UserKey).(*openid.User) 23 | fmt.Fprintf(w, "Hello %v! This is all I know about you: %+v", u.ID, u) 24 | } 25 | 26 | type userHandlerAdapter struct { 27 | h http.HandlerFunc 28 | } 29 | 30 | func (uh userHandlerAdapter) ServeHTTPWithUser(u *openid.User, rw http.ResponseWriter, req *http.Request) { 31 | context.Set(req, UserKey, u) 32 | uh.h.ServeHTTP(rw, req) 33 | } 34 | 35 | func main() { 36 | configuration, _ := openid.NewConfiguration(openid.ProvidersGetter(getProviders_googlePlayground)) 37 | 38 | handlerAdapter := userHandlerAdapter{h: meHandler} 39 | 40 | http.Handle("/me", openid.AuthenticateUser(configuration, handlerAdapter)) 41 | http.Handle("/authn", openid.Authenticate(configuration, http.HandlerFunc(authenticatedHandler))) 42 | http.HandleFunc("/", unauthenticatedHandler) 43 | 44 | http.ListenAndServe(":5100", nil) 45 | } 46 | 47 | // getProviders returns the identity providers that will authenticate the users of the underlying service. 48 | // A Provider is composed by its unique issuer and the collection of client IDs registered with the provider that 49 | // are allowed to call this service. 50 | // On this example Google OP is the provider of choice and the client ID used corresponds 51 | // to the Google OAUTH Playground https://developers.google.com/oauthplayground 52 | func getProviders_googlePlayground() ([]openid.Provider, error) { 53 | provider, err := openid.NewProvider("https://accounts.google.com", []string{"407408718192.apps.googleusercontent.com"}) 54 | 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return []openid.Provider{provider}, nil 60 | } 61 | -------------------------------------------------------------------------------- /openid/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Emanoel Xavier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /openid/README.md: -------------------------------------------------------------------------------- 1 | Go OpenId 2 | =========== 3 | [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/emanoelxavier/openid2go/openid) 4 | [![license](http://img.shields.io/badge/license-MIT-yellowgreen.svg?style=flat)](https://raw.githubusercontent.com/emanoelxavier/openid2go/master/openid/LICENSE) 5 | ## Summary 6 | 7 | A Go package that implements web service middlewares for authenticating identities represented by OpenID Connect (OIDC) ID Tokens. 8 | 9 | "OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. It enables Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server" - [OpenID Connect](http://openid.net/specs/openid-connect-core-1_0.html) 10 | 11 | ## Installation 12 | 13 | go get github.com/emanoelxavier/openid2go/openid 14 | 15 | ## Example 16 | This example demonstrates how to use this package to validate incoming ID Tokens. It initializes the Configuration with the desired providers (OPs) and registers two middlewares: openid.Authenticate and openid.AuthenticateUser. The former performs the token validation while the latter, in addition to that, will forward the user information to the next handler. 17 | 18 | ```go 19 | import ( 20 | "fmt" 21 | "net/http" 22 | 23 | "github.com/emanoelxavier/openid2go/openid" 24 | ) 25 | 26 | func AuthenticatedHandler(w http.ResponseWriter, r *http.Request) { 27 | fmt.Fprintln(w, "The user was authenticated!") 28 | } 29 | 30 | func AuthenticatedHandlerWithUser(u *openid.User, w http.ResponseWriter, r *http.Request) { 31 | fmt.Fprintf(w, "The user was authenticated! The token was issued by %v and the user is %+v.", u.Issuer, u) 32 | } 33 | 34 | func Example() { 35 | configuration, err := openid.NewConfiguration(openid.ProvidersGetter(getProviders_googlePlayground)) 36 | 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | http.Handle("/user", openid.AuthenticateUser(configuration, openid.UserHandlerFunc(AuthenticatedHandlerWithUser))) 42 | http.Handle("/authn", openid.Authenticate(configuration, http.HandlerFunc(AuthenticatedHandler))) 43 | 44 | http.ListenAndServe(":5100", nil) 45 | } 46 | 47 | func myGetProviders() ([]openid.Provider, error) { 48 | provider, err := openid.NewProvider("https://providerissuer", []string{"myClientID"}) 49 | 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return []openid.Provider{provider}, nil 55 | } 56 | ``` 57 | This example is also available in the documentation of this package, for more details see [GoDoc](https://godoc.org/github.com/emanoelxavier/openid2go/openid). 58 | 59 | ## Tests 60 | 61 | #### Unit Tests 62 | ```sh 63 | go test github.com/emanoelxavier/openid2go/openid 64 | ``` 65 | 66 | #### Integration Tests 67 | In addition to to unit tests, this package also comes with integration tests that will validate real ID Tokens issued by real OIDC providers. The following command will run those tests: 68 | 69 | ```sh 70 | go test -tags integration github.com/emanoelxavier/openid2go/openid -issuer=[issuer] -clientID=[clientID] -idToken=[idToken] 71 | ``` 72 | 73 | Replace [issuer], [clientID] and [idToken] with the information from an identity provider of your choice. 74 | 75 | For a quick spin you can use it with tokens issued by Google for the [Google OAuth PlayGround](https://developers.google.com/oauthplayground) entering "openid" (without quotes) within the scope field and copying the issued ID Token. For this provider and client the values will be: 76 | 77 | ```sh 78 | go test -tags integration github.com/emanoelxavier/openid2go/openid -issuer=https://accounts.google.com -clientID=407408718192.apps.googleusercontent.com -idToken=copiedIDToken 79 | ``` 80 | 81 | ## Contributing 82 | 83 | 1. Open an issue if found a bug or have a functional request. 84 | 2. Disccuss. 85 | 3. Branch off, write the fix with test(s) and commit attaching to the issue. 86 | 4. Make a pull request. -------------------------------------------------------------------------------- /openid/configuration.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | type configuration struct { 4 | Issuer string `json:"issuer"` 5 | JwksUri string `json:"jwks_uri"` 6 | } 7 | -------------------------------------------------------------------------------- /openid/configurationgettermock_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import "testing" 4 | 5 | type configurationGetterMock struct { 6 | t *testing.T 7 | Calls chan Call 8 | } 9 | 10 | func newConfigurationGetterMock(t *testing.T) *configurationGetterMock { 11 | return &configurationGetterMock{t, make(chan Call)} 12 | } 13 | 14 | type getConfigurationCall struct { 15 | iss string 16 | } 17 | 18 | type getConfigurationResponse struct { 19 | config configuration 20 | err error 21 | } 22 | 23 | func (c *configurationGetterMock) getConfiguration(iss string) (configuration, error) { 24 | c.Calls <- &getConfigurationCall{iss} 25 | gr := (<-c.Calls).(*getConfigurationResponse) 26 | return gr.config, gr.err 27 | } 28 | 29 | func (c *configurationGetterMock) assertGetConfiguration(iss string, config configuration, err error) { 30 | call := (<-c.Calls).(*getConfigurationCall) 31 | if iss != anything && call.iss != iss { 32 | c.t.Error("Expected getConfiguration with", iss, "but was", call.iss) 33 | } 34 | c.Calls <- &getConfigurationResponse{config, err} 35 | } 36 | 37 | func (c *configurationGetterMock) close() { 38 | close(c.Calls) 39 | } 40 | 41 | func (c *configurationGetterMock) assertDone() { 42 | if _, more := <-c.Calls; more { 43 | c.t.Fatal("Did not expect more calls.") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /openid/configurationprovider.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | const wellKnownOpenIdConfiguration = "/.well-known/openid-configuration" 12 | 13 | type httpGetFunc func(url string) (*http.Response, error) 14 | type decodeResponseFunc func(io.Reader, interface{}) error 15 | 16 | type configurationGetter interface { // Getter 17 | getConfiguration(string) (configuration, error) 18 | } 19 | 20 | type httpConfigurationProvider struct { //configurationProvider 21 | getConfig httpGetFunc //httpGetter 22 | decodeConfig decodeResponseFunc //responseDecoder 23 | } 24 | 25 | func newHTTPConfigurationProvider(gc httpGetFunc, dc decodeResponseFunc) *httpConfigurationProvider { 26 | return &httpConfigurationProvider{gc, dc} 27 | } 28 | 29 | func jsonDecodeResponse(r io.Reader, v interface{}) error { 30 | return json.NewDecoder(r).Decode(v) 31 | } 32 | 33 | func (httpProv *httpConfigurationProvider) getConfiguration(issuer string) (configuration, error) { 34 | // Workaround for tokens issued by google 35 | if issuer == "accounts.google.com" { 36 | issuer = "https://" + issuer 37 | } 38 | 39 | configurationUri := strings.TrimSuffix(issuer, "/") + wellKnownOpenIdConfiguration 40 | var config configuration 41 | resp, err := httpProv.getConfig(configurationUri) 42 | if err != nil { 43 | return config, &ValidationError{Code: ValidationErrorGetOpenIdConfigurationFailure, Message: fmt.Sprintf("Failure while contacting the configuration endpoint %v.", configurationUri), Err: err, HTTPStatus: http.StatusUnauthorized} 44 | } 45 | 46 | defer resp.Body.Close() 47 | 48 | if err := httpProv.decodeConfig(resp.Body, &config); err != nil { 49 | return config, &ValidationError{Code: ValidationErrorDecodeOpenIdConfigurationFailure, Message: fmt.Sprintf("Failure while decoding the configuration retrived from endpoint %v.", configurationUri), Err: err, HTTPStatus: http.StatusUnauthorized} 50 | } 51 | 52 | return config, nil 53 | 54 | } 55 | -------------------------------------------------------------------------------- /openid/configurationprovider_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "testing" 9 | ) 10 | 11 | type testBody struct { 12 | io.Reader 13 | } 14 | 15 | func (testBody) Close() error { return nil } 16 | 17 | func Test_getConfiguration_UsesCorrectUrl(t *testing.T) { 18 | c := NewHTTPClientMock(t) 19 | configurationProvider := httpConfigurationProvider{getConfig: c.httpGet} 20 | 21 | issuer := "https://test" 22 | configSuffix := "/.well-known/openid-configuration" 23 | go func() { 24 | c.assertHttpGet(issuer+configSuffix, nil, errors.New("Read configuration error")) 25 | c.close() 26 | }() 27 | 28 | _, e := configurationProvider.getConfiguration(issuer) 29 | 30 | if e == nil { 31 | t.Error("An error was expected but not returned") 32 | } 33 | 34 | c.assertDone() 35 | } 36 | 37 | func Test_getConfiguration_WhenGetReturnsError(t *testing.T) { 38 | c := NewHTTPClientMock(t) 39 | configurationProvider := httpConfigurationProvider{getConfig: c.httpGet} 40 | 41 | readError := errors.New("Read configuration error") 42 | go func() { 43 | c.assertHttpGet(anything, nil, readError) 44 | c.close() 45 | }() 46 | 47 | _, e := configurationProvider.getConfiguration("issuer") 48 | 49 | expectValidationError(t, e, ValidationErrorGetOpenIdConfigurationFailure, http.StatusUnauthorized, readError) 50 | 51 | c.assertDone() 52 | } 53 | 54 | func Test_getConfiguration_WhenGetSucceeds(t *testing.T) { 55 | c := NewHTTPClientMock(t) 56 | configurationProvider := httpConfigurationProvider{c.httpGet, c.decodeResponse} 57 | 58 | respBody := "openid configuration" 59 | resp := &http.Response{Body: testBody{bytes.NewBufferString(respBody)}} 60 | 61 | go func() { 62 | c.assertHttpGet(anything, resp, nil) 63 | c.assertDecodeResponse(respBody, nil, nil) 64 | c.close() 65 | }() 66 | 67 | _, e := configurationProvider.getConfiguration(anything) 68 | 69 | if e != nil { 70 | t.Error("An error was returned but not expected", e) 71 | } 72 | 73 | c.assertDone() 74 | } 75 | 76 | func Test_getConfiguration_WhenDecodeResponseReturnsError(t *testing.T) { 77 | c := NewHTTPClientMock(t) 78 | configurationProvider := httpConfigurationProvider{c.httpGet, c.decodeResponse} 79 | decodeError := errors.New("Decode configuration error") 80 | respBody := "openid configuration" 81 | resp := &http.Response{Body: testBody{bytes.NewBufferString(respBody)}} 82 | 83 | go func() { 84 | c.assertHttpGet(anything, resp, nil) 85 | c.assertDecodeResponse(anything, nil, decodeError) 86 | c.close() 87 | }() 88 | 89 | _, e := configurationProvider.getConfiguration(anything) 90 | 91 | expectValidationError(t, e, ValidationErrorDecodeOpenIdConfigurationFailure, http.StatusUnauthorized, decodeError) 92 | 93 | c.assertDone() 94 | } 95 | 96 | func Test_getConfiguration_WhenDecodeResponseSucceeds(t *testing.T) { 97 | c := NewHTTPClientMock(t) 98 | configurationProvider := httpConfigurationProvider{c.httpGet, c.decodeResponse} 99 | config := &configuration{"testissuer", "https://testissuer/jwk"} 100 | respBody := "openid configuration" 101 | resp := &http.Response{Body: testBody{bytes.NewBufferString(respBody)}} 102 | 103 | go func() { 104 | c.assertHttpGet(anything, resp, nil) 105 | c.assertDecodeResponse(anything, config, nil) 106 | c.close() 107 | }() 108 | 109 | rc, e := configurationProvider.getConfiguration(anything) 110 | 111 | if e != nil { 112 | t.Error("An error was returned but not expected", e) 113 | } 114 | 115 | if rc.Issuer != config.Issuer { 116 | t.Error("Expected issuer", config.Issuer, "but was", rc.Issuer) 117 | } 118 | 119 | if rc.JwksUri != config.JwksUri { 120 | t.Error("Expected jwks uri", config.JwksUri, "but was", rc.JwksUri) 121 | } 122 | 123 | c.assertDone() 124 | } 125 | 126 | func expectValidationError(t *testing.T, e error, vec ValidationErrorCode, status int, inner error) { 127 | if e == nil { 128 | t.Error("An error was expected but not returned") 129 | } 130 | 131 | if ve, ok := e.(*ValidationError); ok { 132 | if ve.Code != vec { 133 | t.Error("Expected error code", vec, "but was", ve.Code) 134 | } 135 | if ve.HTTPStatus != status { 136 | t.Error("Expected HTTP status", status, "but was", ve.HTTPStatus) 137 | } 138 | if inner != nil && ve.Err.Error() != inner.Error() { 139 | t.Error("Expected inner error", inner.Error(), ",but was", ve.Err.Error()) 140 | } 141 | } else { 142 | t.Errorf("Expected error type '*ValidationError' but was %T", e) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /openid/doc.go: -------------------------------------------------------------------------------- 1 | /*Package openid implements web service middlewares for authenticating identities represented by 2 | OpenID Connect (OIDC) ID Tokens. 3 | For details on OIDC go to http://openid.net/specs/openid-connect-core-1_0.html 4 | 5 | The middlewares will: extract the ID Token from the request; retrieve the OIDC provider (OP) 6 | configuration and signing keys; validate the token and provide the user identity and claims to the 7 | underlying web service. 8 | 9 | The Basics 10 | 11 | At the core of this package are the Authenticate and AuthenticateUser middlewares. To use either one 12 | of them you will need an instance of the Configuration type, to create that you use NewConfiguration. 13 | 14 | func Authenticate(conf *Configuration, h http.Handler) http.Handler 15 | func AuthenticateUser(conf *Configuration, h UserHandler) http.Handler 16 | NewConfiguration(options ...option) (*Configuration, error) 17 | 18 | // options: 19 | 20 | func ErrorHandler(eh ErrorHandlerFunc) func(*Configuration) error 21 | func ProvidersGetter(pg GetProvidersFunc) func(*Configuration) error 22 | 23 | // extension points: 24 | 25 | type ErrorHandlerFunc func(error, http.ResponseWriter, *http.Request) bool 26 | type GetProvidersFunc func() ([]Provider, error) 27 | 28 | The Example below demonstrates these elements working together. 29 | 30 | Token Parsing 31 | 32 | Both Authenticate and AuthenticateUser middlewares expect the incoming requests to have an HTTP 33 | Authorization header with the content 'Bearer [idToken]' where [idToken] is a valid ID Token issued by 34 | an OP. For instance: 35 | 36 | Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6... 37 | 38 | By default, requests that do not contain an Authorization header with this content will not be forwarded 39 | to the next HTTP handler in the pipeline, instead they will fail back to the client with HTTP status 40 | 400/Bad Request. 41 | 42 | Token Validation 43 | 44 | Once parsed the ID Token will be validated: 45 | 46 | 1) Is the token a valid jwt? 47 | 2) Is the token issued by a known OP? 48 | 3) Is the token issued for a known client? 49 | 4) Is the token valid at the time ('not use before' and 'expire at' claims)? 50 | 5) Is the token signed accordingly? 51 | 52 | The signature validation is done with the public keys retrieved from the jwks_uri published by the OP in 53 | its OIDC metadata (https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). 54 | 55 | The token's issuer and audiences will be verified using a collection of the type Provider. This 56 | collection is retrieved by calling the implementation of the function GetProvidersFunc registered with 57 | the Configuration. 58 | If the token issuer matches the Issuer of any of the providers and the token audience matches at least 59 | one of the ClientIDs of the respective provider then the token is considered valid. 60 | 61 | func myGetProviders() ([]openid.Provider, error) { 62 | p, err := openid.NewProvider("https://accounts.google.com", 63 | []string{"407408718192.apps.googleusercontent.com"}) 64 | // .... 65 | return []openid.Provider{p}, nil 66 | } 67 | 68 | c, _ := openid.NewConfiguration(openid.ProvidersGetter(myGetProviders)) 69 | 70 | In code above only tokens with Issuer claim ('iss') https://accounts.google.com and Audiences claim 71 | ('aud') containing "407408718192.apps.googleusercontent.com" can be valid. 72 | 73 | By default, when the token validation fails for any reason the requests will not be forwarded to the next 74 | handler in the pipeline, instead they will fail back to the client with HTTP status 401/Unauthorized. 75 | 76 | Error Handling 77 | 78 | The default behavior of the Authenticate and AuthenticateUser middlewares upon error conditions is: 79 | the execution pipeline is stopped (the next handler will not be executed), the response will contain 80 | status 400 when a token is not found and 401 when it is invalid, and the response will also contain the 81 | error message. 82 | This behavior can be changed by implementing a function of type ErrorHandlerFunc and registering it 83 | using ErrorHandler with the Configuration. 84 | 85 | type ErrorHandlerFunc func(error, http.ResponseWriter, *http.Request) bool 86 | func ErrorHandler(eh ErrorHandlerFunc) func(*Configuration) error 87 | 88 | For instance: 89 | 90 | func myErrorHandler(e error, w http.ResponseWriter, r *http.Request) bool { 91 | fmt.Fprintf(w, e.Error()) 92 | return false 93 | } 94 | 95 | c, _ := openid.NewConfiguration(openid.ProvidersGetter(myGetProviders), 96 | openid.ErrorHandler(myErrorHandler)) 97 | 98 | In the code above myErrorHandler adds the error message to the response and let the execution 99 | continue to the next handler in the pipeline (returning false) for all error types. 100 | You can use this extension point to fine tune what happens when a specific error is returned by your 101 | implementation of the GetProvidersFunc or even for the error types and codes exported by this 102 | package: 103 | 104 | type ValidationError struct 105 | type ValidationErrorCode uint32 106 | type SetupError struct 107 | type SetupErrorCode uint32 108 | 109 | Authenticate vs AuthenticateUser 110 | 111 | Both middlewares Authenticate and AuthenticateUser behave exactly the same way when it comes to 112 | parsing and validating the ID Token. The only difference is that AuthenticateUser will forward the 113 | information about the user's identity from the ID Token to the next handler in the pipeline. 114 | If your service does not need to know the identity of the authenticated user then Authenticate will 115 | suffice, otherwise your choice is AuthenticateUser. 116 | In order to receive the User information from the AuthenticateUser the next handler in the pipeline 117 | must implement the interface UserHandler with the following function: 118 | 119 | ServeHTTPWithUser(*User, http.ResponseWriter, *http.Request) 120 | 121 | You can also make use of the function adapter UserHandlerFunc as shown in the example below: 122 | 123 | func myHandlerWithUser(u *openid.User, w http.ResponseWriter, r *http.Request) { 124 | fmt.Fprintf(w, "Authenticated! The user is %+v.", u) 125 | } 126 | 127 | http.Handle("/user", openid.AuthenticateUser(c, openid.UserHandlerFunc(myHandlerWithUser))) 128 | */ 129 | package openid 130 | -------------------------------------------------------------------------------- /openid/errors.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/golang-jwt/jwt/v4" 8 | ) 9 | 10 | // SetupErrorCode is the type of error code that can 11 | // be returned by the operations done during middleware setup. 12 | type SetupErrorCode uint32 13 | 14 | // Setup error constants. 15 | const ( 16 | SetupErrorInvalidIssuer SetupErrorCode = iota // Invalid issuer provided during setup. 17 | SetupErrorInvalidClientIDs // Invalid client id collection provided during setup. 18 | SetupErrorEmptyProviderCollection // Empty collection of providers provided during setup. 19 | ) 20 | 21 | // ValidationErrorCode is the type of error code that can 22 | // be returned by the operations done during token validation. 23 | type ValidationErrorCode uint32 24 | 25 | // Validation error constants. 26 | const ( 27 | ValidationErrorAuthorizationHeaderNotFound ValidationErrorCode = iota // Authorization header not found on request. 28 | ValidationErrorAuthorizationHeaderWrongFormat // Authorization header unexpected format. 29 | ValidationErrorAuthorizationHeaderWrongSchemeName // Authorization header unexpected scheme. 30 | ValidationErrorJwtValidationFailure // Jwt token validation failed with a known error. 31 | ValidationErrorJwtValidationUnknownFailure // Jwt token validation failed with an unknown error. 32 | ValidationErrorInvalidAudienceType // Unexpected token audience type. 33 | ValidationErrorInvalidAudience // Unexpected token audience content. 34 | ValidationErrorAudienceNotFound // Unexpected token audience value. Audience not registered. 35 | ValidationErrorInvalidIssuerType // Unexpected token issuer type. 36 | ValidationErrorInvalidIssuer // Unexpected token issuer content. 37 | ValidationErrorIssuerNotFound // Unexpected token value. Issuer not registered. 38 | ValidationErrorGetOpenIdConfigurationFailure // Failure while retrieving the OIDC configuration. 39 | ValidationErrorDecodeOpenIdConfigurationFailure // Failure while decoding the OIDC configuration. 40 | ValidationErrorGetJwksFailure // Failure while retrieving jwk set. 41 | ValidationErrorDecodeJwksFailure // Failure while decoding the jwk set. 42 | ValidationErrorEmptyJwk // Empty jwk returned. 43 | ValidationErrorEmptyJwkKey // Empty jwk key set returned. 44 | ValidationErrorMarshallingKey // Error while marshalling the signing key. 45 | ValidationErrorKidNotFound // Key identifier not found. 46 | ValidationErrorInvalidSubjectType // Unexpected token subject type. 47 | ValidationErrorInvalidSubject // Unexpected token subject content. 48 | ValidationErrorSubjectNotFound // Token missing the 'sub' claim. 49 | ValidationErrorIdTokenEmpty // Empty ID token. 50 | ValidationErrorEmptyProviders // Empty collection of providers. 51 | ) 52 | 53 | const setupErrorMessagePrefix string = "Setup Error." 54 | const validationErrorMessagePrefix string = "Validation Error." 55 | 56 | // SetupError represents the error returned by operations called during 57 | // middleware setup. 58 | type SetupError struct { 59 | Err error 60 | Code SetupErrorCode 61 | Message string 62 | } 63 | 64 | // Error returns a formatted string containing the error Message. 65 | func (se SetupError) Error() string { 66 | return fmt.Sprintf("Setup error. %v", se.Message) 67 | } 68 | 69 | // ValidationError represents the error returned by operations called during 70 | // token validation. 71 | type ValidationError struct { 72 | Err error 73 | Code ValidationErrorCode 74 | Message string 75 | HTTPStatus int 76 | } 77 | 78 | // The ErrorHandlerFunc represents the function used to handle errors during token 79 | // validation. Applications can have their own implementation of this function and 80 | // register it using the ErrorHandler option. Through this extension point applications 81 | // can choose what to do upon different error types, for instance return an certain HTTP Status code 82 | // and/or include some detailed message in the response. 83 | // This function returns false if the next handler registered after the ID Token validation 84 | // should be executed when an error is found or true if the execution should be stopped. 85 | type ErrorHandlerFunc func(error, http.ResponseWriter, *http.Request) bool 86 | 87 | // Error returns a formatted string containing the error Message. 88 | func (ve ValidationError) Error() string { 89 | return fmt.Sprintf("Validation error. %v", ve.Message) 90 | } 91 | 92 | // jwtErrorToOpenIdError converts errors of the type *jwt.ValidationError returned during token validation into errors of type *ValidationError 93 | func jwtErrorToOpenIdError(e error) *ValidationError { 94 | if jwtError, ok := e.(*jwt.ValidationError); ok { 95 | if (jwtError.Errors & (jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired | jwt.ValidationErrorSignatureInvalid)) != 0 { 96 | return &ValidationError{Code: ValidationErrorJwtValidationFailure, Message: "Jwt token validation failed.", HTTPStatus: http.StatusUnauthorized} 97 | } 98 | 99 | if (jwtError.Errors & jwt.ValidationErrorMalformed) != 0 { 100 | return &ValidationError{Code: ValidationErrorJwtValidationFailure, Message: "Jwt token validation failed.", HTTPStatus: http.StatusBadRequest} 101 | } 102 | 103 | if (jwtError.Errors & jwt.ValidationErrorUnverifiable) != 0 { 104 | // TODO: improve this once https://github.com/dgrijalva/jwt-go/issues/108 is resolved. 105 | // Currently jwt.Parse does not surface errors returned by the KeyFunc. 106 | return &ValidationError{Code: ValidationErrorJwtValidationFailure, Message: jwtError.Error(), HTTPStatus: http.StatusUnauthorized} 107 | } 108 | } 109 | 110 | return &ValidationError{Code: ValidationErrorJwtValidationUnknownFailure, Message: "Jwt token validation failed with unknown error.", HTTPStatus: http.StatusInternalServerError} 111 | } 112 | 113 | func validationErrorToHTTPStatus(e error, rw http.ResponseWriter, req *http.Request) (halt bool) { 114 | if verr, ok := e.(*ValidationError); ok { 115 | http.Error(rw, verr.Message, verr.HTTPStatus) 116 | } else { 117 | rw.WriteHeader(http.StatusInternalServerError) 118 | fmt.Fprintf(rw, e.Error()) 119 | } 120 | 121 | return true 122 | } 123 | -------------------------------------------------------------------------------- /openid/example_test.go: -------------------------------------------------------------------------------- 1 | package openid_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/TykTechnologies/openid2go/openid" 8 | ) 9 | 10 | func AuthenticatedHandler(w http.ResponseWriter, r *http.Request) { 11 | fmt.Fprintln(w, "The user was authenticated!") 12 | } 13 | 14 | func AuthenticatedHandlerWithUser(u *openid.User, w http.ResponseWriter, r *http.Request) { 15 | fmt.Fprintf(w, "The user was authenticated! The token was issued by %v and the user is %+v.", u.Issuer, u) 16 | } 17 | 18 | func UnauthenticatedHandler(w http.ResponseWriter, r *http.Request) { 19 | fmt.Fprintln(w, "Reached without authentication!") 20 | } 21 | 22 | // This example demonstrates how to use of the openid middlewares to validate incoming 23 | // ID Tokens in the HTTP Authorization header with the format 'Bearer id_token'. 24 | // It initializes the Configuration with the desired providers (OPs) and registers two 25 | // middlewares: openid.Authenticate and openid.AuthenticateUser. 26 | // The former will validate the ID Token and fail the call if the token is not valid. 27 | // The latter will do the same but forward the user's information extracted from the token to the next handler. 28 | func Example() { 29 | configuration, err := openid.NewConfiguration(openid.ProvidersGetter(getProviders_googlePlayground)) 30 | 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | http.Handle("/user", openid.AuthenticateUser(configuration, openid.UserHandlerFunc(AuthenticatedHandlerWithUser))) 36 | http.Handle("/authn", openid.Authenticate(configuration, http.HandlerFunc(AuthenticatedHandler))) 37 | http.HandleFunc("/unauth", UnauthenticatedHandler) 38 | 39 | http.ListenAndServe(":5100", nil) 40 | } 41 | 42 | // getProviders returns the identity providers that will authenticate the users of the underlying service. 43 | // A Provider is composed by its unique issuer and the collection of client IDs registered with the provider that 44 | // are allowed to call this service. 45 | // On this example Google OP is the provider of choice and the client ID used corresponds 46 | // to the Google OAUTH Playground https://developers.google.com/oauthplayground 47 | func getProviders_googlePlayground() ([]openid.Provider, error) { 48 | provider, err := openid.NewProvider("https://accounts.google.com", []string{"407408718192.apps.googleusercontent.com"}) 49 | 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return []openid.Provider{provider}, nil 55 | } 56 | -------------------------------------------------------------------------------- /openid/httpclientmock_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/go-jose/go-jose/v3" 10 | ) 11 | 12 | type Call interface{} 13 | 14 | const anything = "anything" 15 | 16 | type HTTPClientMock struct { 17 | t *testing.T 18 | Calls chan Call 19 | } 20 | 21 | func NewHTTPClientMock(t *testing.T) *HTTPClientMock { 22 | return &HTTPClientMock{t, make(chan Call)} 23 | } 24 | 25 | type httpGetCall struct { 26 | url string 27 | } 28 | 29 | type httpGetResp struct { 30 | resp *http.Response 31 | err error 32 | } 33 | 34 | type decodeResponseCall struct { 35 | reader io.Reader 36 | } 37 | 38 | type decodeResponseResp struct { 39 | value interface{} 40 | err error 41 | } 42 | 43 | func (c *HTTPClientMock) httpGet(url string) (*http.Response, error) { 44 | c.Calls <- &httpGetCall{url} 45 | gr := (<-c.Calls).(*httpGetResp) 46 | return gr.resp, gr.err 47 | } 48 | 49 | func (c *HTTPClientMock) assertHttpGet(url string, resp *http.Response, err error) { 50 | call := (<-c.Calls).(*httpGetCall) 51 | if url != anything && call.url != url { 52 | c.t.Error("Expected httpGet with", url, "but was", call.url) 53 | } 54 | c.Calls <- &httpGetResp{resp, err} 55 | } 56 | 57 | func (c *HTTPClientMock) decodeResponse(reader io.Reader, value interface{}) error { 58 | c.Calls <- &decodeResponseCall{reader} 59 | dr := (<-c.Calls).(*decodeResponseResp) 60 | switch v := value.(type) { 61 | case *configuration: 62 | if dr.value != nil { 63 | v.Issuer = dr.value.(*configuration).Issuer 64 | v.JwksUri = dr.value.(*configuration).JwksUri 65 | } 66 | case *jose.JSONWebKeySet: 67 | if dr.value != nil { 68 | v.Keys = dr.value.(*jose.JSONWebKeySet).Keys 69 | } 70 | 71 | default: 72 | c.t.Fatalf("Expected value type '*configuration', but was %T", value) 73 | 74 | } 75 | 76 | return dr.err 77 | } 78 | 79 | func (c *HTTPClientMock) assertDecodeResponse(response string, value interface{}, err error) { 80 | call := (<-c.Calls).(*decodeResponseCall) 81 | if response != anything { 82 | b, e := ioutil.ReadAll(call.reader) 83 | if e != nil { 84 | c.t.Error("Error while reading from the call reader", e) 85 | } 86 | s := string(b) 87 | 88 | if s != response { 89 | c.t.Error("Expected decodeResponse with", response, "but was", s) 90 | } 91 | } 92 | 93 | c.Calls <- &decodeResponseResp{value, err} 94 | } 95 | 96 | func (c *HTTPClientMock) close() { 97 | close(c.Calls) 98 | } 99 | 100 | func (c *HTTPClientMock) assertDone() { 101 | if _, more := <-c.Calls; more { 102 | c.t.Fatal("Did not expect more calls.") 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /openid/idtokenvalidator.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/golang-jwt/jwt/v4" 8 | ) 9 | 10 | const issuerClaimName = "iss" 11 | const audiencesClaimName = "aud" 12 | const subjectClaimName = "sub" 13 | const keyIDJwtHeaderName = "kid" 14 | 15 | type JWTTokenValidator interface { 16 | Validate(t string) (jt *jwt.Token, err error) 17 | } 18 | 19 | type jwtParserFunc func(string, jwt.Keyfunc, ...jwt.ParserOption) (*jwt.Token, error) 20 | 21 | type idTokenValidator struct { 22 | provGetter GetProvidersFunc 23 | jwtParser jwtParserFunc 24 | keyGetter signingKeyGetter 25 | } 26 | 27 | func newIDTokenValidator(pg GetProvidersFunc, jp jwtParserFunc, kg signingKeyGetter) *idTokenValidator { 28 | return &idTokenValidator{pg, jp, kg} 29 | } 30 | 31 | func (tv *idTokenValidator) Validate(t string) (*jwt.Token, error) { 32 | jt, err := tv.jwtParser(t, tv.getSigningKey) 33 | if err != nil { 34 | if verr, ok := err.(*jwt.ValidationError); ok { 35 | // If the signing key did not match it may be because the in memory key is outdated. 36 | // Renew the cached signing key. 37 | if (verr.Errors & jwt.ValidationErrorSignatureInvalid) != 0 { 38 | jt, err = tv.jwtParser(t, tv.renewAndGetSigningKey) 39 | } 40 | } 41 | } 42 | 43 | if err != nil { 44 | return nil, jwtErrorToOpenIdError(err) 45 | } 46 | 47 | return jt, nil 48 | } 49 | 50 | func (tv *idTokenValidator) renewAndGetSigningKey(jt *jwt.Token) (interface{}, error) { 51 | // Issuer is already validated when 'getSigningKey was called. 52 | iss := jt.Claims.(jwt.MapClaims)[issuerClaimName].(string) 53 | 54 | err := tv.keyGetter.flushCachedSigningKeys(iss) 55 | 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | headerVal, ok := jt.Header[keyIDJwtHeaderName] 61 | 62 | if !ok { 63 | return tv.keyGetter.getSigningKey(iss, "") 64 | } 65 | 66 | switch headerVal.(type) { 67 | case string: 68 | return tv.keyGetter.getSigningKey(iss, headerVal.(string)) 69 | default: 70 | return tv.keyGetter.getSigningKey(iss, "") 71 | } 72 | 73 | } 74 | 75 | func (tv *idTokenValidator) getSigningKey(jt *jwt.Token) (interface{}, error) { 76 | provs, err := tv.provGetter() 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | if err := providers(provs).validate(); err != nil { 82 | return nil, err 83 | } 84 | 85 | p, err := validateIssuer(jt, provs) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | _, err = validateAudiences(jt, p) 91 | if err != nil { 92 | return nil, err 93 | } 94 | _, err = validateSubject(jt) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | var kid string = "" 100 | 101 | if jt.Header[keyIDJwtHeaderName] != nil { 102 | kid = jt.Header[keyIDJwtHeaderName].(string) 103 | } 104 | 105 | return tv.keyGetter.getSigningKey(p.Issuer, kid) 106 | } 107 | 108 | func validateIssuer(jt *jwt.Token, ps []Provider) (*Provider, error) { 109 | issuerClaim := getIssuer(jt) 110 | var ti string 111 | 112 | if iss, ok := issuerClaim.(string); ok { 113 | ti = iss 114 | } else { 115 | return nil, &ValidationError{Code: ValidationErrorInvalidIssuerType, Message: fmt.Sprintf("Invalid Issuer type: %T", issuerClaim), HTTPStatus: http.StatusUnauthorized} 116 | } 117 | 118 | if ti == "" { 119 | return nil, &ValidationError{Code: ValidationErrorInvalidIssuer, Message: "The token 'iss' claim was not found or was empty.", HTTPStatus: http.StatusUnauthorized} 120 | } 121 | 122 | // Workaround for tokens issued by google 123 | gi := ti 124 | if gi == "accounts.google.com" { 125 | gi = "https://" + gi 126 | } 127 | 128 | for _, p := range ps { 129 | if ti == p.Issuer || gi == p.Issuer { 130 | return &p, nil 131 | } 132 | } 133 | 134 | return nil, &ValidationError{Code: ValidationErrorIssuerNotFound, Message: fmt.Sprintf("No provider was registered with issuer: %v", ti), HTTPStatus: http.StatusUnauthorized} 135 | } 136 | 137 | func validateSubject(jt *jwt.Token) (string, error) { 138 | subjectClaim := getSubject(jt) 139 | 140 | var ts string 141 | if sub, ok := subjectClaim.(string); ok { 142 | ts = sub 143 | } else { 144 | return ts, &ValidationError{Code: ValidationErrorInvalidSubjectType, Message: fmt.Sprintf("Invalid subject type: %T", subjectClaim), HTTPStatus: http.StatusUnauthorized} 145 | } 146 | 147 | if ts == "" { 148 | return ts, &ValidationError{Code: ValidationErrorInvalidSubject, Message: "The token 'sub' claim was not found or was empty.", HTTPStatus: http.StatusUnauthorized} 149 | } 150 | 151 | return ts, nil 152 | } 153 | 154 | func validateAudiences(jt *jwt.Token, p *Provider) (string, error) { 155 | audiencesClaim, err := getAudiences(jt) 156 | 157 | if err != nil { 158 | return "", err 159 | } 160 | 161 | for _, aud := range p.ClientIDs { 162 | for _, audienceClaim := range audiencesClaim { 163 | ta, ok := audienceClaim.(string) 164 | if !ok { 165 | fmt.Printf("aud type %T \n", audienceClaim) 166 | return "", &ValidationError{Code: ValidationErrorInvalidAudienceType, Message: fmt.Sprintf("Invalid Audiences type: %T", audiencesClaim), HTTPStatus: http.StatusUnauthorized} 167 | } 168 | 169 | if ta == "" { 170 | return "", &ValidationError{Code: ValidationErrorInvalidAudience, Message: "The token 'aud' claim was not found or was empty.", HTTPStatus: http.StatusUnauthorized} 171 | } 172 | 173 | if ta == aud { 174 | return ta, nil 175 | } 176 | } 177 | } 178 | 179 | return "", &ValidationError{Code: ValidationErrorAudienceNotFound, Message: fmt.Sprintf("The provider %v does not have a client id matching any of the token audiences %+v", p.Issuer, audiencesClaim), HTTPStatus: http.StatusUnauthorized} 180 | } 181 | 182 | func getAudiences(t *jwt.Token) ([]interface{}, error) { 183 | audiencesClaim := t.Claims.(jwt.MapClaims)[audiencesClaimName] 184 | if aud, ok := audiencesClaim.(string); ok { 185 | return []interface{}{aud}, nil 186 | } else if _, ok := audiencesClaim.([]interface{}); ok { 187 | return audiencesClaim.([]interface{}), nil 188 | } 189 | 190 | return nil, &ValidationError{Code: ValidationErrorInvalidAudienceType, Message: fmt.Sprintf("Invalid Audiences type: %T", audiencesClaim), HTTPStatus: http.StatusUnauthorized} 191 | 192 | } 193 | 194 | func getIssuer(t *jwt.Token) interface{} { 195 | return t.Claims.(jwt.MapClaims)[issuerClaimName] 196 | } 197 | 198 | func getSubject(t *jwt.Token) interface{} { 199 | return t.Claims.(jwt.MapClaims)[subjectClaimName] 200 | } 201 | -------------------------------------------------------------------------------- /openid/idtokenvalidator_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/golang-jwt/jwt/v4" 9 | ) 10 | 11 | func Test_getSigningKey_WhenGetProvidersReturnsError(t *testing.T) { 12 | pm, _, _, tv := createIDTokenValidator(t) 13 | 14 | ee := errors.New("Error getting providers.") 15 | 16 | go func() { 17 | pm.assertGetProviders(nil, ee) 18 | pm.close() 19 | }() 20 | 21 | sk, err := tv.getSigningKey(nil) 22 | 23 | if sk != nil { 24 | t.Error("The returned signing key should be nil.") 25 | } 26 | 27 | if err == nil { 28 | t.Fatal("An error was expected but not returned.") 29 | } 30 | 31 | if err.Error() != ee.Error() { 32 | t.Error("Expected error", ee, ", but got", err) 33 | } 34 | 35 | pm.assertDone() 36 | } 37 | 38 | func Test_getSigningKey_WhenGetProvidersReturnsEmptyCollection(t *testing.T) { 39 | pm, _, _, tv := createIDTokenValidator(t) 40 | 41 | go func() { 42 | pm.assertGetProviders(nil, nil) 43 | pm.assertGetProviders([]Provider{}, nil) 44 | pm.close() 45 | }() 46 | 47 | _, err := tv.getSigningKey(nil) 48 | expectSetupError(t, err, SetupErrorEmptyProviderCollection) 49 | 50 | _, err = tv.getSigningKey(nil) 51 | expectSetupError(t, err, SetupErrorEmptyProviderCollection) 52 | 53 | pm.assertDone() 54 | 55 | } 56 | 57 | func Test_getSigningKey_UsingTokenWithInvalidIssuerType(t *testing.T) { 58 | pm, _, _, tv := createIDTokenValidator(t) 59 | 60 | go func() { 61 | pm.assertGetProviders([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil) 62 | pm.close() 63 | }() 64 | 65 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{}))} 66 | jt.Claims.(jwt.MapClaims)["iss"] = 0 // The expected issuer type is string, not int. 67 | sk, err := tv.getSigningKey(jt) 68 | 69 | if sk != nil { 70 | t.Error("The returned signing key should be nil.") 71 | } 72 | 73 | expectValidationError(t, err, ValidationErrorInvalidIssuerType, http.StatusUnauthorized, nil) 74 | pm.assertDone() 75 | } 76 | 77 | func Test_getSigningKey_UsingTokenWithEmptyIssuer(t *testing.T) { 78 | pm, _, _, tv := createIDTokenValidator(t) 79 | 80 | go func() { 81 | pm.assertGetProviders([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil) 82 | pm.assertGetProviders([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil) 83 | 84 | pm.close() 85 | }() 86 | 87 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{}))} 88 | 89 | // The token has no 'iss' claim 90 | sk, err := tv.getSigningKey(jt) 91 | 92 | if sk != nil { 93 | t.Error("The returned signing key should be nil.") 94 | } 95 | 96 | expectValidationError(t, err, ValidationErrorInvalidIssuerType, http.StatusUnauthorized, nil) 97 | 98 | // The token has '' as 'iss' claim 99 | jt.Claims.(jwt.MapClaims)["iss"] = "" 100 | sk, err = tv.getSigningKey(jt) 101 | 102 | if sk != nil { 103 | t.Error("The returned signing key should be nil.") 104 | } 105 | 106 | expectValidationError(t, err, ValidationErrorInvalidIssuer, http.StatusUnauthorized, nil) 107 | 108 | pm.assertDone() 109 | } 110 | 111 | func Test_getSigningKey_UsingTokenWithUnknownIssuer(t *testing.T) { 112 | pm, _, _, tv := createIDTokenValidator(t) 113 | 114 | go func() { 115 | pm.assertGetProviders([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil) 116 | pm.close() 117 | }() 118 | 119 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{}))} 120 | jt.Claims.(jwt.MapClaims)["iss"] = "http://unknown" 121 | 122 | // The token has no 'iss' claim 123 | sk, err := tv.getSigningKey(jt) 124 | 125 | if sk != nil { 126 | t.Error("The returned signing key should be nil.") 127 | } 128 | 129 | expectValidationError(t, err, ValidationErrorIssuerNotFound, http.StatusUnauthorized, nil) 130 | pm.assertDone() 131 | } 132 | 133 | func Test_getSigningKey_UsingTokenWithInvalidAudienceType(t *testing.T) { 134 | pm, _, _, tv := createIDTokenValidator(t) 135 | 136 | go func() { 137 | pm.assertGetProviders([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil) 138 | pm.close() 139 | }() 140 | 141 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{}))} 142 | jt.Claims.(jwt.MapClaims)["iss"] = "https://issuer" 143 | jt.Claims.(jwt.MapClaims)["aud"] = 0 // Expected 'aud' type is string 144 | 145 | sk, err := tv.getSigningKey(jt) 146 | 147 | if sk != nil { 148 | t.Error("The returned signing key should be nil.") 149 | } 150 | 151 | expectValidationError(t, err, ValidationErrorInvalidAudienceType, http.StatusUnauthorized, nil) 152 | pm.assertDone() 153 | } 154 | 155 | func Test_getSigningKey_UsingTokenWithInvalidAudience(t *testing.T) { 156 | pm, _, _, tv := createIDTokenValidator(t) 157 | 158 | go func() { 159 | pm.assertGetProviders([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil) 160 | pm.assertGetProviders([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil) 161 | pm.close() 162 | }() 163 | 164 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{}))} 165 | jt.Claims.(jwt.MapClaims)["iss"] = "https://issuer" 166 | 167 | // No audience claim 168 | sk, err := tv.getSigningKey(jt) 169 | 170 | if sk != nil { 171 | t.Error("The returned signing key should be nil.") 172 | } 173 | 174 | expectValidationError(t, err, ValidationErrorInvalidAudienceType, http.StatusUnauthorized, nil) 175 | 176 | // Empty audience claim. 177 | jt.Claims.(jwt.MapClaims)["aud"] = "" 178 | sk, err = tv.getSigningKey(jt) 179 | 180 | if sk != nil { 181 | t.Error("The returned signing key should be nil.") 182 | } 183 | 184 | expectValidationError(t, err, ValidationErrorInvalidAudience, http.StatusUnauthorized, nil) 185 | pm.assertDone() 186 | 187 | } 188 | 189 | func Test_getSigningKey_UsingTokenWithUnknownAudience(t *testing.T) { 190 | pm, _, _, tv := createIDTokenValidator(t) 191 | 192 | go func() { 193 | pm.assertGetProviders([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client1", "client2"}}}, nil) 194 | pm.close() 195 | }() 196 | 197 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{}))} 198 | jt.Claims.(jwt.MapClaims)["iss"] = "https://issuer" 199 | jt.Claims.(jwt.MapClaims)["aud"] = "client3" // unknown audience 200 | 201 | sk, err := tv.getSigningKey(jt) 202 | 203 | if sk != nil { 204 | t.Error("The returned signing key should be nil.") 205 | } 206 | 207 | expectValidationError(t, err, ValidationErrorAudienceNotFound, http.StatusUnauthorized, nil) 208 | pm.assertDone() 209 | } 210 | 211 | func Test_getSigningKey_UsingTokenWithUnknownMultipleAudiences(t *testing.T) { 212 | pm, _, _, tv := createIDTokenValidator(t) 213 | 214 | go func() { 215 | pm.assertGetProviders([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client1", "client2"}}}, nil) 216 | pm.close() 217 | }() 218 | 219 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{}))} 220 | jt.Claims.(jwt.MapClaims)["iss"] = "https://issuer" 221 | jt.Claims.(jwt.MapClaims)["aud"] = []interface{}{"client3", "client4"} // unknown audiences 222 | 223 | sk, err := tv.getSigningKey(jt) 224 | 225 | if sk != nil { 226 | t.Error("The returned signing key should be nil.") 227 | } 228 | 229 | expectValidationError(t, err, ValidationErrorAudienceNotFound, http.StatusUnauthorized, nil) 230 | pm.assertDone() 231 | } 232 | 233 | func Test_getSigningKey_UsingTokenWithInvalidSubjectType(t *testing.T) { 234 | pm, _, _, tv := createIDTokenValidator(t) 235 | 236 | go func() { 237 | pm.assertGetProviders([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil) 238 | pm.close() 239 | }() 240 | 241 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{}))} 242 | jt.Claims.(jwt.MapClaims)["iss"] = "https://issuer" 243 | jt.Claims.(jwt.MapClaims)["aud"] = "client" 244 | jt.Claims.(jwt.MapClaims)["sub"] = 0 // The expected 'sub' claim type is string 245 | sk, err := tv.getSigningKey(jt) 246 | 247 | if sk != nil { 248 | t.Error("The returned signing key should be nil.") 249 | } 250 | 251 | expectValidationError(t, err, ValidationErrorInvalidSubjectType, http.StatusUnauthorized, nil) 252 | pm.assertDone() 253 | } 254 | 255 | func Test_getSigningKey_UsingValidToken_WhenSigningKeyGetterReturnsError(t *testing.T) { 256 | pm, _, sm, tv := createIDTokenValidator(t) 257 | 258 | iss := "https://issuer" 259 | keyID := "kid" 260 | ee := &ValidationError{Code: ValidationErrorIssuerNotFound, HTTPStatus: http.StatusUnauthorized} 261 | 262 | go func() { 263 | pm.assertGetProviders([]Provider{{Issuer: iss, ClientIDs: []string{"client"}}}, nil) 264 | sm.assertGetSigningKey(iss, keyID, nil, ee) 265 | pm.close() 266 | sm.close() 267 | }() 268 | 269 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{})), Header: make(map[string]interface{})} 270 | jt.Claims.(jwt.MapClaims)["iss"] = iss 271 | jt.Claims.(jwt.MapClaims)["aud"] = "client" 272 | jt.Claims.(jwt.MapClaims)["sub"] = "subject1" 273 | jt.Header["kid"] = keyID 274 | 275 | _, err := tv.getSigningKey(jt) 276 | 277 | expectValidationError(t, err, ee.Code, ee.HTTPStatus, nil) 278 | pm.assertDone() 279 | sm.assertDone() 280 | } 281 | 282 | func Test_getSigningKey_UsingValidToken_WhenSigningKeyGetterSucceeds(t *testing.T) { 283 | pm, _, sm, tv := createIDTokenValidator(t) 284 | 285 | iss := "https://issuer" 286 | keyID := "kid" 287 | esk := "signingKey" 288 | 289 | go func() { 290 | pm.assertGetProviders([]Provider{{Issuer: iss, ClientIDs: []string{"client"}}}, nil) 291 | sm.assertGetSigningKey(iss, keyID, []byte(esk), nil) 292 | pm.close() 293 | sm.close() 294 | }() 295 | 296 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{})), Header: make(map[string]interface{})} 297 | jt.Claims.(jwt.MapClaims)["iss"] = iss 298 | jt.Claims.(jwt.MapClaims)["aud"] = "client" 299 | jt.Claims.(jwt.MapClaims)["sub"] = "subject1" 300 | jt.Header["kid"] = keyID 301 | 302 | rsk, err := tv.getSigningKey(jt) 303 | 304 | if err != nil { 305 | t.Error("An error was returned but not expected.", err) 306 | } 307 | 308 | expectSigningKey(t, rsk, jt, esk) 309 | 310 | pm.assertDone() 311 | sm.assertDone() 312 | } 313 | 314 | func Test_getSigningKey_UsingValidToken_WithoutKeyIdentifier_WhenSigningKeyGetterSucceeds(t *testing.T) { 315 | pm, _, sm, tv := createIDTokenValidator(t) 316 | 317 | iss := "https://issuer" 318 | keyID := "" 319 | esk := "signingKey" 320 | 321 | go func() { 322 | pm.assertGetProviders([]Provider{{Issuer: iss, ClientIDs: []string{"client"}}}, nil) 323 | sm.assertGetSigningKey(iss, keyID, []byte(esk), nil) 324 | pm.close() 325 | sm.close() 326 | }() 327 | 328 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{})), Header: make(map[string]interface{})} 329 | jt.Claims.(jwt.MapClaims)["iss"] = iss 330 | jt.Claims.(jwt.MapClaims)["aud"] = "client" 331 | jt.Claims.(jwt.MapClaims)["sub"] = "subject1" 332 | 333 | rsk, err := tv.getSigningKey(jt) 334 | 335 | if err != nil { 336 | t.Error("An error was returned but not expected.", err) 337 | } 338 | 339 | expectSigningKey(t, rsk, jt, esk) 340 | 341 | pm.assertDone() 342 | sm.assertDone() 343 | } 344 | 345 | func Test_getSigningKey_UsingValidTokenWithMultipleAudiences(t *testing.T) { 346 | pm, _, sm, tv := createIDTokenValidator(t) 347 | 348 | iss := "https://issuer" 349 | keyID := "kid" 350 | esk := "signingKey" 351 | 352 | go func() { 353 | pm.assertGetProviders([]Provider{{Issuer: iss, ClientIDs: []string{"client"}}}, nil) 354 | sm.assertGetSigningKey(iss, keyID, []byte(esk), nil) 355 | pm.close() 356 | sm.close() 357 | }() 358 | 359 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{})), Header: make(map[string]interface{})} 360 | jt.Claims.(jwt.MapClaims)["iss"] = iss 361 | jt.Claims.(jwt.MapClaims)["aud"] = []interface{}{"unknown", "client"} 362 | jt.Claims.(jwt.MapClaims)["sub"] = "subject1" 363 | jt.Header["kid"] = keyID 364 | 365 | rsk, err := tv.getSigningKey(jt) 366 | 367 | if err != nil { 368 | t.Error("An error was returned but not expected.", err) 369 | } 370 | 371 | expectSigningKey(t, rsk, jt, esk) 372 | 373 | pm.assertDone() 374 | sm.assertDone() 375 | } 376 | 377 | func Test_renewAndGetSigningKey_UsingValidToken_WhenFlushCachedSigningKeysReturnsError(t *testing.T) { 378 | _, _, sm, tv := createIDTokenValidator(t) 379 | 380 | ee := &ValidationError{Code: ValidationErrorIssuerNotFound, HTTPStatus: http.StatusUnauthorized} 381 | go func() { 382 | sm.assertFlushCachedSigningKeys(anything, ee) 383 | sm.close() 384 | }() 385 | 386 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{})), Header: make(map[string]interface{})} 387 | jt.Claims.(jwt.MapClaims)["iss"] = "" 388 | 389 | _, err := tv.renewAndGetSigningKey(jt) 390 | 391 | expectValidationError(t, err, ee.Code, ee.HTTPStatus, nil) 392 | 393 | sm.assertDone() 394 | } 395 | 396 | func Test_renewAndGetSigningKey_UsingValidToken_WhenGetSigningKeyReturnsError(t *testing.T) { 397 | _, _, sm, tv := createIDTokenValidator(t) 398 | 399 | ee := &ValidationError{Code: ValidationErrorIssuerNotFound, HTTPStatus: http.StatusUnauthorized} 400 | go func() { 401 | sm.assertFlushCachedSigningKeys(anything, nil) 402 | sm.assertGetSigningKey(anything, anything, nil, ee) 403 | sm.close() 404 | }() 405 | 406 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{})), Header: make(map[string]interface{})} 407 | jt.Claims.(jwt.MapClaims)["iss"] = "" 408 | jt.Header["kid"] = "" 409 | 410 | _, err := tv.renewAndGetSigningKey(jt) 411 | 412 | expectValidationError(t, err, ee.Code, ee.HTTPStatus, nil) 413 | 414 | sm.assertDone() 415 | } 416 | 417 | func Test_renewAndGetSigningKey_UsingValidToken_WhenGetSigningKeySucceeds(t *testing.T) { 418 | _, _, sm, tv := createIDTokenValidator(t) 419 | esk := "signingKey" 420 | 421 | go func() { 422 | sm.assertFlushCachedSigningKeys(anything, nil) 423 | sm.assertGetSigningKey(anything, anything, []byte(esk), nil) 424 | sm.close() 425 | }() 426 | 427 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{})), Header: make(map[string]interface{})} 428 | jt.Claims.(jwt.MapClaims)["iss"] = "" 429 | jt.Header["kid"] = "" 430 | 431 | rsk, err := tv.renewAndGetSigningKey(jt) 432 | 433 | if err != nil { 434 | t.Error("An error was returned but not expected.", err) 435 | } 436 | 437 | expectSigningKey(t, rsk, jt, esk) 438 | 439 | sm.assertDone() 440 | } 441 | 442 | func Test_validate_WhenParserReturnsErrorFirstTime(t *testing.T) { 443 | _, jm, _, tv := createIDTokenValidator(t) 444 | 445 | je := &jwt.ValidationError{Errors: jwt.ValidationErrorNotValidYet} 446 | ee := &ValidationError{Code: ValidationErrorJwtValidationFailure, HTTPStatus: http.StatusUnauthorized} 447 | 448 | go func() { 449 | jm.assertParse(anything, nil, je) 450 | jm.close() 451 | }() 452 | 453 | _, err := tv.Validate(anything) 454 | 455 | expectValidationError(t, err, ee.Code, ee.HTTPStatus, ee.Err) 456 | 457 | jm.assertDone() 458 | } 459 | 460 | func Test_validate_WhenParserSuceedsFirstTime(t *testing.T) { 461 | _, jm, _, tv := createIDTokenValidator(t) 462 | 463 | jt := &jwt.Token{} 464 | 465 | go func() { 466 | jm.assertParse(anything, jt, nil) 467 | jm.close() 468 | }() 469 | 470 | rjt, err := tv.Validate(anything) 471 | 472 | if err != nil { 473 | t.Error("Unexpected error was returned.", err) 474 | } 475 | 476 | if rjt != jt { 477 | t.Errorf("Expected %+v, but got %+v.", jt, rjt) 478 | } 479 | 480 | jm.assertDone() 481 | } 482 | 483 | func Test_validate_WhenParserReturnsErrorSecondTime(t *testing.T) { 484 | _, jm, _, tv := createIDTokenValidator(t) 485 | 486 | jfe := &jwt.ValidationError{Errors: jwt.ValidationErrorSignatureInvalid} 487 | je := &jwt.ValidationError{Errors: jwt.ValidationErrorMalformed} 488 | ee := &ValidationError{Code: ValidationErrorJwtValidationFailure, HTTPStatus: http.StatusBadRequest} 489 | 490 | go func() { 491 | jm.assertParse(anything, nil, jfe) 492 | jm.assertParse(anything, nil, je) 493 | jm.close() 494 | }() 495 | 496 | _, err := tv.Validate(anything) 497 | 498 | expectValidationError(t, err, ee.Code, ee.HTTPStatus, ee.Err) 499 | 500 | jm.assertDone() 501 | } 502 | 503 | func Test_validate_WhenParserReturnsSignatureInvalidErrorSecondTime(t *testing.T) { 504 | _, jm, _, tv := createIDTokenValidator(t) 505 | 506 | je := &jwt.ValidationError{Errors: jwt.ValidationErrorSignatureInvalid} 507 | ee := &ValidationError{Code: ValidationErrorJwtValidationFailure, HTTPStatus: http.StatusUnauthorized} 508 | 509 | go func() { 510 | jm.assertParse(anything, nil, je) 511 | jm.assertParse(anything, nil, je) 512 | jm.close() 513 | }() 514 | 515 | _, err := tv.Validate(anything) 516 | 517 | expectValidationError(t, err, ee.Code, ee.HTTPStatus, ee.Err) 518 | 519 | jm.assertDone() 520 | } 521 | 522 | func Test_validate_WhenParserSuceedsSecondTime(t *testing.T) { 523 | _, jm, _, tv := createIDTokenValidator(t) 524 | 525 | jfe := &jwt.ValidationError{Errors: jwt.ValidationErrorSignatureInvalid} 526 | 527 | jt := &jwt.Token{} 528 | 529 | go func() { 530 | jm.assertParse(anything, jt, jfe) 531 | jm.assertParse(anything, jt, nil) 532 | jm.close() 533 | }() 534 | 535 | rjt, err := tv.Validate(anything) 536 | 537 | if err != nil { 538 | t.Error("Unexpected error was returned.", err) 539 | } 540 | 541 | if rjt != jt { 542 | t.Errorf("Expected %+v, but got %+v.", jt, rjt) 543 | } 544 | 545 | jm.assertDone() 546 | } 547 | 548 | func expectSigningKey(t *testing.T, rsk interface{}, jt *jwt.Token, esk string) { 549 | 550 | if rsk == nil { 551 | t.Fatal("The returned signing key was nil.") 552 | } 553 | 554 | if skb, ok := rsk.([]byte); ok { 555 | rsks := string(skb) 556 | if rsks != esk { 557 | t.Error("Expected signing key", esk, "but got", rsks) 558 | } 559 | } else { 560 | t.Errorf("Expected signing key type '[]byte', but got %T", rsk) 561 | } 562 | } 563 | 564 | func createIDTokenValidator(t *testing.T) (*providersGetterMock, *jwtParserMock, *signingKeyGetterMock, *idTokenValidator) { 565 | pm := newProvidersGetterMock(t) 566 | jm := newJwtParserMock(t) 567 | sm := newSigningKeyGetterMock(t) 568 | return pm, jm, sm, &idTokenValidator{pm.getProviders, jm.parse, sm} 569 | } 570 | -------------------------------------------------------------------------------- /openid/integration_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package openid_test 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/http/httptest" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/TykTechnologies/openid2go/openid" 15 | ) 16 | 17 | const authenticatedMessage string = "Congrats, you are authenticated!" 18 | const authenticatedMessageWithUser string = "Congrats, you are authenticated by the provider %v!" 19 | 20 | // The idToken flag must have a valid ID Token issued by any OIDC provider. 21 | var idToken = flag.String("idToken", "", "a valid id token") 22 | 23 | // The issuer and cliendID flags must be valid. 24 | var issuer = flag.String("issuer", "", "the OP issuer") 25 | var clientID = flag.String("clientID", "", "the client ID registered with the OP") 26 | 27 | var mux *http.ServeMux 28 | 29 | // The authenticateHandler is registered behind the openid.Authenticate middleware. 30 | func authenticatedHandler(w http.ResponseWriter, r *http.Request) { 31 | fmt.Fprintln(w, authenticatedMessage) 32 | } 33 | 34 | // The authenticateHandlerWithUser is registered behind the openid.AuthenticateUser middleware. 35 | func authenticatedHandlerWithUser(u *openid.User, w http.ResponseWriter, r *http.Request) { 36 | iss := u.Issuer 37 | 38 | // Workaround for Google OP since it has a bug causing the 'iss' claim to miss the 'https://' 39 | if strings.HasPrefix(*issuer, "https://") && !strings.HasPrefix(iss, "https://") { 40 | iss = "https://" + iss 41 | } 42 | 43 | fmt.Fprintf(w, "%v User: %+v.\n", fmt.Sprintf(authenticatedMessageWithUser, iss), u) 44 | } 45 | 46 | // The init func initializes the openid.Configuration and the server routes. 47 | func init() { 48 | mux = http.NewServeMux() 49 | config, err := openid.NewConfiguration(openid.ProvidersGetter(getProviders)) 50 | 51 | if err != nil { 52 | fmt.Println("Error whe creating the configuration for the openid middleware.", err) 53 | } 54 | 55 | mux.Handle("/authn", openid.Authenticate(config, http.HandlerFunc(authenticatedHandler))) 56 | mux.Handle("/user", openid.AuthenticateUser(config, openid.UserHandlerFunc(authenticatedHandlerWithUser))) 57 | } 58 | 59 | // Validates that a valid ID Token results in a successful authentication of the user. 60 | func Test_Authenticate_ValidIDToken(t *testing.T) { 61 | // Arrange. 62 | server := httptest.NewServer(mux) 63 | defer server.Close() 64 | 65 | // Act. 66 | res, code, err := executeRequest(server.URL, "/authn", *idToken) 67 | 68 | // Assert. 69 | validateResponse(t, err, res, code, authenticatedMessage, http.StatusOK) 70 | } 71 | 72 | // Validates that an ID Token signed by an unknown key identifier results in HTTP Status Unauthorized. 73 | func Test_Authenticate_InvalidIDTokenKeyID(t *testing.T) { 74 | // Arrange. 75 | server := httptest.NewServer(mux) 76 | defer server.Close() 77 | et := "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNiMzVkMTZjZmI4MWY2ZTUzZDk5YTBmODg4YjRhZTgyNWE3MWU1Y2MifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXRfaGFzaCI6Im1iVVZpRlFReUFPX2Y1YlR0alVvREEiLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTI2Nzg1OTg3MTA3MDYxNzA2NDkiLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJpYXQiOjE0NTA5MjkxMjAsImV4cCI6MTQ1MDkzMjcyMH0.f5toakDvtU3Tqt71uDgIACrac8mGM4K8HQ1Fyw9jaUdxonEu_Bww-UNKjPD6tKAe7AzVJzfKOzzcvJygMRfQ72u4wsljhQV3i6-cJmpMj4S5HQoleV4GqNHq-84KNEvFv_4IT7wIEdu0kEpRygt9lhysvFXxGfkR6TpTr50W8yo4T0EfRVXafXhNMX5uNkVJ2PnHQXYvwiZP2_hJ6KTw9RN_4mDJIOHJBAXRXnBe9hJ7pDfED1v_ayOJGmq5PGeAO34Rz7FDf4Awf8DOoMiRVSi3SwE_pRHwBRp2WsrvKpSdUToXyOmBEzPYubEJIcYPiJR4uXgPEOynV0i993NGQA" 78 | 79 | // Changing the client ID and issuer to match the ones in the token. 80 | cID := "407408718192.apps.googleusercontent.com" 81 | cIDp := &cID 82 | cIDp, clientID = clientID, cIDp 83 | iss := "accounts.google.com" 84 | issp := &iss 85 | issp, issuer = issuer, issp 86 | 87 | // Act. 88 | res, code, err := executeRequest(server.URL, "/user", et) 89 | 90 | // Assert. 91 | validateResponse(t, err, res, code, "", http.StatusUnauthorized) 92 | 93 | // Clean up. 94 | // Swapping back the values for the next test. 95 | clientID = cIDp 96 | issuer = issp 97 | } 98 | 99 | // Validates that an ID Token issued for an audience that is not registered as an 100 | // allowed client ID returns unauthorized. This test also demonstrates that configuration 101 | // changes takes effect without the need of a service start. 102 | func Test_Authenticate_ChangeClientID_InvalidateIDToken(t *testing.T) { 103 | // Arrange. 104 | server := httptest.NewServer(mux) 105 | defer server.Close() 106 | 107 | // Changing client ID for another value to invalidate the token. 108 | cID := "newClientID" 109 | cIDp := &cID 110 | cIDp, clientID = clientID, cIDp 111 | 112 | // Act. 113 | res, code, err := executeRequest(server.URL, "/user", *idToken) 114 | 115 | // Assert. 116 | validateResponse(t, err, res, code, "", http.StatusUnauthorized) 117 | 118 | // Clean up. 119 | // Swapping back the values for the next test. 120 | clientID = cIDp 121 | } 122 | 123 | // Validates that a valid ID Token results in a successful authentication of the user. 124 | // And that the user information extracted from the token is made available to the rest of the 125 | // application stack. 126 | func Test_AuthenticateUser_ValidIDToken(t *testing.T) { 127 | // Arrange. 128 | server := httptest.NewServer(mux) 129 | defer server.Close() 130 | 131 | // Act. 132 | res, code, err := executeRequest(server.URL, "/user", *idToken) 133 | 134 | // Assert. 135 | validateResponse(t, err, res, code, fmt.Sprintf(authenticatedMessageWithUser, *issuer), http.StatusOK) 136 | } 137 | 138 | // getProviders is used as the GetProvidersFunc when creating a new openid.Configuration. 139 | func getProviders() ([]openid.Provider, error) { 140 | return []openid.Provider{{Issuer: *issuer, ClientIDs: []string{*clientID}}}, nil 141 | } 142 | 143 | func executeRequest(u string, r string, t string) (string, int, error) { 144 | var res string 145 | var code int 146 | client := http.DefaultClient 147 | 148 | req, err := http.NewRequest("GET", u+r, nil) 149 | if err != nil { 150 | return res, code, err 151 | } 152 | 153 | req.Header.Add("Authorization", "Bearer "+t) 154 | 155 | resp, err := client.Do(req) 156 | if err != nil { 157 | return res, code, err 158 | } 159 | 160 | msg, err := ioutil.ReadAll(resp.Body) 161 | resp.Body.Close() 162 | 163 | if err != nil { 164 | return res, code, err 165 | } 166 | 167 | res = string(msg) 168 | code = resp.StatusCode 169 | 170 | return res, code, nil 171 | } 172 | 173 | func validateResponse(t *testing.T, e error, r string, c int, er string, ec int) { 174 | if e != nil { 175 | t.Error(e) 176 | } 177 | 178 | if er != "" && !strings.HasPrefix(r, er) { 179 | t.Error("Expected response with prefix:", er, "but got:", r) 180 | } else { 181 | t.Log(r) 182 | } 183 | 184 | if c != ec { 185 | t.Error("Expected HTTP status", ec, "but got", c) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /openid/jwksgettermock_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-jose/go-jose/v3" 7 | ) 8 | 9 | type jwksGetterMock struct { 10 | t *testing.T 11 | Calls chan Call 12 | } 13 | 14 | func newJwksGetterMock(t *testing.T) *jwksGetterMock { 15 | return &jwksGetterMock{t, make(chan Call)} 16 | } 17 | 18 | type getJwksCall struct { 19 | url string 20 | } 21 | 22 | type getJwksResponse struct { 23 | jwks jose.JSONWebKeySet 24 | err error 25 | } 26 | 27 | func (c *jwksGetterMock) getJwkSet(url string) (jose.JSONWebKeySet, error) { 28 | c.Calls <- &getJwksCall{url} 29 | gr := (<-c.Calls).(*getJwksResponse) 30 | return gr.jwks, gr.err 31 | } 32 | 33 | func (c *jwksGetterMock) assertGetJwks(url string, jwks jose.JSONWebKeySet, err error) { 34 | call := (<-c.Calls).(*getJwksCall) 35 | if url != anything && call.url != url { 36 | c.t.Error("Expected getJwks with", url, "but was", call.url) 37 | } 38 | c.Calls <- &getJwksResponse{jwks, err} 39 | } 40 | 41 | func (c *jwksGetterMock) close() { 42 | close(c.Calls) 43 | } 44 | 45 | func (c *jwksGetterMock) assertDone() { 46 | if _, more := <-c.Calls; more { 47 | c.t.Fatal("Did not expect more calls.") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /openid/jwksprovider.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-jose/go-jose/v3" 8 | ) 9 | 10 | type jwksGetter interface { 11 | getJwkSet(string) (jose.JSONWebKeySet, error) 12 | } 13 | 14 | type httpJwksProvider struct { 15 | getJwks httpGetFunc 16 | decodeJwks decodeResponseFunc 17 | } 18 | 19 | func newHTTPJwksProvider(gf httpGetFunc, df decodeResponseFunc) *httpJwksProvider { 20 | return &httpJwksProvider{gf, df} 21 | } 22 | 23 | func (httpProv *httpJwksProvider) getJwkSet(url string) (jose.JSONWebKeySet, error) { 24 | var jwks jose.JSONWebKeySet 25 | resp, err := httpProv.getJwks(url) 26 | if err != nil { 27 | return jwks, &ValidationError{Code: ValidationErrorGetJwksFailure, Message: fmt.Sprintf("Failure while contacting the jwk endpoint %v: %v", url, err), Err: err, HTTPStatus: http.StatusUnauthorized} 28 | } 29 | 30 | defer resp.Body.Close() 31 | 32 | if err := httpProv.decodeJwks(resp.Body, &jwks); err != nil { 33 | return jwks, &ValidationError{Code: ValidationErrorDecodeJwksFailure, Message: fmt.Sprintf("Failure while decoding the jwk retrieved from the endpoint %v: %v", url, err), Err: err, HTTPStatus: http.StatusUnauthorized} 34 | } 35 | 36 | return jwks, nil 37 | } 38 | -------------------------------------------------------------------------------- /openid/jwksprovider_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/go-jose/go-jose/v3" 10 | ) 11 | 12 | func Test_getJwkSet_UsesCorrectUrl(t *testing.T) { 13 | c := NewHTTPClientMock(t) 14 | jwksProvider := httpJwksProvider{getJwks: c.httpGet} 15 | 16 | url := "https://jwks" 17 | 18 | go func() { 19 | c.assertHttpGet(url, nil, errors.New("Read configuration error")) 20 | c.close() 21 | }() 22 | 23 | _, e := jwksProvider.getJwkSet(url) 24 | 25 | if e == nil { 26 | t.Error("An error was expected but not returned") 27 | } 28 | 29 | c.assertDone() 30 | } 31 | 32 | func Test_getJwkSet_WhenGetReturnsError(t *testing.T) { 33 | c := NewHTTPClientMock(t) 34 | jwksProvider := httpJwksProvider{getJwks: c.httpGet} 35 | 36 | readError := errors.New("Read jwks error") 37 | go func() { 38 | c.assertHttpGet(anything, nil, readError) 39 | c.close() 40 | }() 41 | 42 | _, e := jwksProvider.getJwkSet(anything) 43 | 44 | expectValidationError(t, e, ValidationErrorGetJwksFailure, http.StatusUnauthorized, readError) 45 | 46 | c.assertDone() 47 | } 48 | 49 | func Test_getJwkSet_WhenGetSucceeds(t *testing.T) { 50 | c := NewHTTPClientMock(t) 51 | jwksProvider := httpJwksProvider{c.httpGet, c.decodeResponse} 52 | 53 | respBody := "jwk set" 54 | resp := &http.Response{Body: testBody{bytes.NewBufferString(respBody)}} 55 | 56 | go func() { 57 | c.assertHttpGet(anything, resp, nil) 58 | c.assertDecodeResponse(respBody, nil, nil) 59 | c.close() 60 | }() 61 | 62 | _, e := jwksProvider.getJwkSet(anything) 63 | 64 | if e != nil { 65 | t.Error("An error was returned but not expected", e) 66 | } 67 | 68 | c.assertDone() 69 | } 70 | 71 | func Test_getJwkSet_WhenDecodeResponseReturnsError(t *testing.T) { 72 | c := NewHTTPClientMock(t) 73 | jwksProvider := httpJwksProvider{c.httpGet, c.decodeResponse} 74 | decodeError := errors.New("Decode jwks error") 75 | respBody := "jwk set." 76 | resp := &http.Response{Body: testBody{bytes.NewBufferString(respBody)}} 77 | 78 | go func() { 79 | c.assertHttpGet(anything, resp, nil) 80 | c.assertDecodeResponse(anything, nil, decodeError) 81 | c.close() 82 | }() 83 | 84 | _, e := jwksProvider.getJwkSet(anything) 85 | 86 | expectValidationError(t, e, ValidationErrorDecodeJwksFailure, http.StatusUnauthorized, decodeError) 87 | 88 | c.assertDone() 89 | } 90 | 91 | func Test_getJwkSet_WhenDecodeResponseSucceeds(t *testing.T) { 92 | c := NewHTTPClientMock(t) 93 | jwksProvider := httpJwksProvider{c.httpGet, c.decodeResponse} 94 | keys := []jose.JSONWebKey{ 95 | {Key: "key1", Certificates: nil, KeyID: "keyid1", Algorithm: "algo1", Use: "use1"}, 96 | {Key: "key2", Certificates: nil, KeyID: "keyid2", Algorithm: "algo2", Use: "use2"}, 97 | } 98 | jwks := &jose.JSONWebKeySet{Keys: keys} 99 | respBody := "jwk set" 100 | resp := &http.Response{Body: testBody{bytes.NewBufferString(respBody)}} 101 | 102 | go func() { 103 | c.assertHttpGet(anything, resp, nil) 104 | c.assertDecodeResponse(anything, jwks, nil) 105 | c.close() 106 | }() 107 | 108 | rj, e := jwksProvider.getJwkSet(anything) 109 | 110 | if e != nil { 111 | t.Error("An error was returned but not expected", e) 112 | } 113 | 114 | if len(rj.Keys) != len(jwks.Keys) { 115 | t.Fatal("Expected", len(jwks.Keys), "keys, but got", len(rj.Keys)) 116 | } 117 | 118 | for i, key := range rj.Keys { 119 | ek := jwks.Keys[i] 120 | if key.Algorithm != ek.Algorithm { 121 | t.Errorf("Key algorithm at %v should be %v, but was %v", i, ek.Algorithm, key.Algorithm) 122 | } 123 | if key.KeyID != ek.KeyID { 124 | t.Errorf("Key ID at %v should be %v, but was %v", i, ek.KeyID, key.KeyID) 125 | } 126 | if key.Key != ek.Key { 127 | t.Errorf("Key at %v should be %v, but was %v", i, ek.Key, key.Key) 128 | } 129 | if key.Use != ek.Use { 130 | t.Errorf("Key Use at %v should be %v, but was %v", i, ek.Use, key.Use) 131 | } 132 | } 133 | 134 | c.assertDone() 135 | } 136 | -------------------------------------------------------------------------------- /openid/jwtparsermock_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang-jwt/jwt/v4" 7 | ) 8 | 9 | type jwtParserMock struct { 10 | t *testing.T 11 | Calls chan Call 12 | } 13 | 14 | func newJwtParserMock(t *testing.T) *jwtParserMock { 15 | return &jwtParserMock{t, make(chan Call)} 16 | } 17 | 18 | type parseCall struct { 19 | t string 20 | kf jwt.Keyfunc 21 | } 22 | 23 | type parseResp struct { 24 | jt *jwt.Token 25 | e error 26 | } 27 | 28 | func (p *jwtParserMock) parse(t string, kf jwt.Keyfunc, _ ...jwt.ParserOption) (*jwt.Token, error) { 29 | p.Calls <- &parseCall{t, kf} 30 | pr := (<-p.Calls).(*parseResp) 31 | return pr.jt, pr.e 32 | } 33 | 34 | func (p *jwtParserMock) assertParse(t string, jt *jwt.Token, e error) { 35 | call := (<-p.Calls).(*parseCall) 36 | if call.t != anything && t != call.t { 37 | p.t.Error("Expected parse with", t, "but was", call.t) 38 | } 39 | 40 | p.Calls <- &parseResp{jt, e} 41 | } 42 | 43 | func (p *jwtParserMock) close() { 44 | close(p.Calls) 45 | } 46 | 47 | func (p *jwtParserMock) assertDone() { 48 | if _, more := <-p.Calls; more { 49 | p.t.Fatal("Did not expect more calls.") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /openid/jwttokenvalidatormock_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang-jwt/jwt/v4" 7 | ) 8 | 9 | type jwtTokenValidatorMock struct { 10 | t *testing.T 11 | Calls chan Call 12 | } 13 | 14 | func newJwtTokenValidatorMock(t *testing.T) *jwtTokenValidatorMock { 15 | return &jwtTokenValidatorMock{t, make(chan Call)} 16 | } 17 | 18 | type validateCall struct { 19 | t string 20 | } 21 | 22 | type validateResp struct { 23 | jt *jwt.Token 24 | err error 25 | } 26 | 27 | func (j *jwtTokenValidatorMock) Validate(t string) (*jwt.Token, error) { 28 | j.Calls <- &validateCall{t} 29 | vr := (<-j.Calls).(*validateResp) 30 | return vr.jt, vr.err 31 | } 32 | 33 | func (j *jwtTokenValidatorMock) assertValidate(t string, jt *jwt.Token, err error) { 34 | call := (<-j.Calls).(*validateCall) 35 | if t != anything && call.t != t { 36 | j.t.Error("Expected validate with token", t, "but was", call.t) 37 | } 38 | j.Calls <- &validateResp{jt, err} 39 | } 40 | 41 | func (j *jwtTokenValidatorMock) close() { 42 | close(j.Calls) 43 | } 44 | 45 | func (j *jwtTokenValidatorMock) assertDone() { 46 | if _, more := <-j.Calls; more { 47 | j.t.Fatal("Did not expect more calls.") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /openid/middleware.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/golang-jwt/jwt/v4" 7 | ) 8 | 9 | // The Configuration contains the entities needed to perform ID token validation. 10 | // This type should be instantiated at the application startup time. 11 | type Configuration struct { 12 | tokenValidator JWTTokenValidator 13 | IDTokenGetter GetIDTokenFunc 14 | errorHandler ErrorHandlerFunc 15 | } 16 | 17 | type option func(*Configuration) error 18 | 19 | // The NewConfiguration creates a new instance of Configuration and returns a pointer to it. 20 | // This function receives a collection of the function type option. Each of those functions are 21 | // responsible for setting some part of the returned *Configuration. If any if the option functions 22 | // returns an error then NewConfiguration will return a nil configuration and that error. 23 | func NewConfiguration(options ...option) (*Configuration, error) { 24 | m := new(Configuration) 25 | cp := newHTTPConfigurationProvider(http.Get, jsonDecodeResponse) 26 | jp := newHTTPJwksProvider(http.Get, jsonDecodeResponse) 27 | ksp := newSigningKeySetProvider(cp, jp, pemEncodePublicKey) 28 | kp := newSigningKeyProvider(ksp) 29 | m.tokenValidator = newIDTokenValidator(nil, jwt.Parse, kp) 30 | 31 | for _, option := range options { 32 | err := option(m) 33 | 34 | if err != nil { 35 | return nil, err 36 | } 37 | } 38 | 39 | return m, nil 40 | } 41 | 42 | // The ProvidersGetter option registers the function responsible for returning the 43 | // providers containing the valid issuer and client IDs used to validate the ID Token. 44 | func ProvidersGetter(pg GetProvidersFunc) func(*Configuration) error { 45 | return func(c *Configuration) error { 46 | c.tokenValidator.(*idTokenValidator).provGetter = pg 47 | return nil 48 | } 49 | } 50 | 51 | func TokenValidator(tv JWTTokenValidator) func(*Configuration) error { 52 | return func(c *Configuration) error { 53 | c.tokenValidator = tv 54 | return nil 55 | } 56 | } 57 | 58 | // The ErrorHandler option registers the function responsible for handling 59 | // the errors returned during token validation. When this option is not used then the 60 | // middleware will use the default internal implementation validationErrorToHTTPStatus. 61 | func ErrorHandler(eh ErrorHandlerFunc) func(*Configuration) error { 62 | return func(c *Configuration) error { 63 | c.errorHandler = eh 64 | return nil 65 | } 66 | } 67 | 68 | // The Authenticate middleware performs the validation of the OIDC ID Token. 69 | // If an error happens, i.e.: expired token, the next handler may or may not executed depending on the 70 | // provided ErrorHandlerFunc option. The default behavior, determined by validationErrorToHTTPStatus, 71 | // stops the execution and returns Unauthorized. 72 | // If the validation is successful then the next handler(h) will be executed. 73 | func Authenticate(conf *Configuration, h http.Handler) http.Handler { 74 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 75 | if _, halt := authenticate(conf, w, r); !halt { 76 | h.ServeHTTP(w, r) 77 | } 78 | }) 79 | } 80 | 81 | // The AuthenticateUser middleware performs the validation of the OIDC ID Token and 82 | // forwards the authenticated user's information to the next handler in the pipeline. 83 | // If an error happens, i.e.: expired token, the next handler may or may not executed depending on the 84 | // provided ErrorHandlerFunc option. The default behavior, determined by validationErrorToHTTPStatus, 85 | // stops the execution and returns Unauthorized. 86 | // If the validation is successful then the next handler(h) will be executed and will 87 | // receive the authenticated user information. 88 | func AuthenticateUser(conf *Configuration, h UserHandler) http.Handler { 89 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 90 | if u, halt := authenticateUser(conf, w, r); !halt { 91 | h.ServeHTTPWithUser(u, w, r) 92 | } 93 | }) 94 | } 95 | 96 | // Exported authenticate so we don't need to use the middleware 97 | func AuthenticateOIDWithUser(c *Configuration, rw http.ResponseWriter, req *http.Request) (*User, *jwt.Token, bool) { 98 | return authenticateUserWithToken(c, rw, req) 99 | } 100 | 101 | func authenticate(c *Configuration, rw http.ResponseWriter, req *http.Request) (t *jwt.Token, halt bool) { 102 | var tg GetIDTokenFunc 103 | if c.IDTokenGetter == nil { 104 | tg = getIDTokenAuthorizationHeader 105 | } else { 106 | tg = c.IDTokenGetter 107 | } 108 | 109 | var eh ErrorHandlerFunc 110 | if c.errorHandler == nil { 111 | eh = validationErrorToHTTPStatus 112 | } else { 113 | eh = c.errorHandler 114 | } 115 | 116 | ts, err := tg(req) 117 | 118 | if err != nil { 119 | return nil, eh(err, rw, req) 120 | } 121 | 122 | vt, err := c.tokenValidator.Validate(ts) 123 | 124 | if err != nil { 125 | return nil, eh(err, rw, req) 126 | } 127 | 128 | return vt, false 129 | } 130 | 131 | func authenticateUser(c *Configuration, rw http.ResponseWriter, req *http.Request) (u *User, halt bool) { 132 | var vt *jwt.Token 133 | 134 | var eh ErrorHandlerFunc 135 | if c.errorHandler == nil { 136 | eh = validationErrorToHTTPStatus 137 | } else { 138 | eh = c.errorHandler 139 | } 140 | 141 | if t, h := authenticate(c, rw, req); h { 142 | return nil, h 143 | } else { 144 | vt = t 145 | } 146 | 147 | u, err := newUser(vt) 148 | 149 | if err != nil { 150 | return nil, eh(err, rw, req) 151 | } 152 | 153 | return u, false 154 | } 155 | 156 | func authenticateUserWithToken(c *Configuration, rw http.ResponseWriter, req *http.Request) (u *User, vt *jwt.Token, halt bool) { 157 | var eh ErrorHandlerFunc 158 | if c.errorHandler == nil { 159 | eh = validationErrorToHTTPStatus 160 | } else { 161 | eh = c.errorHandler 162 | } 163 | 164 | if t, h := authenticate(c, rw, req); h { 165 | return nil, nil, h 166 | } else { 167 | vt = t 168 | } 169 | 170 | u, err := newUser(vt) 171 | 172 | if err != nil { 173 | return nil, nil, eh(err, rw, req) 174 | } 175 | 176 | return u, vt, false 177 | } 178 | -------------------------------------------------------------------------------- /openid/middleware_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/golang-jwt/jwt/v4" 10 | ) 11 | 12 | const idToken string = "IDTOKEN" 13 | 14 | func Test_authenticateUser_WhenGetIDTokenReturnsError_WhenErrorHandlerContinues(t *testing.T) { 15 | _, c := createConfiguration(t, errorHandlerContinue, getIDTokenReturnsError) 16 | 17 | u, halt := authenticateUser(c, httptest.NewRecorder(), nil) 18 | 19 | if u != nil { 20 | t.Errorf("The returned user should be nil, but was %+v.", u) 21 | } 22 | 23 | if halt { 24 | t.Error("The authentication should have returned 'halt' false.") 25 | } 26 | } 27 | 28 | func Test_authenticateUser_WhenGetIDTokenReturnsError_WhenErrorHandlerHalts(t *testing.T) { 29 | _, c := createConfiguration(t, errorHandlerHalt, getIDTokenReturnsError) 30 | 31 | u, halt := authenticateUser(c, httptest.NewRecorder(), nil) 32 | 33 | if u != nil { 34 | t.Errorf("The returned user should be nil, but was %+v.", u) 35 | } 36 | 37 | if !halt { 38 | t.Error("The authentication should have returned 'halt' true.") 39 | } 40 | } 41 | 42 | func Test_authenticateUser_WhenValidateReturnsError_WhenErrorHandlerHalts(t *testing.T) { 43 | vm, c := createConfiguration(t, errorHandlerHalt, getIDTokenReturnsSuccess) 44 | go func() { 45 | vm.assertValidate(idToken, nil, errors.New("Error while validating the token.")) 46 | vm.close() 47 | }() 48 | 49 | u, halt := authenticateUser(c, httptest.NewRecorder(), nil) 50 | 51 | if u != nil { 52 | t.Errorf("The returned user should be nil, but was %+v.", u) 53 | } 54 | 55 | if !halt { 56 | t.Error("The authentication should have returned 'halt' true.") 57 | } 58 | 59 | vm.assertDone() 60 | } 61 | 62 | func Test_authenticateUser_WhenValidateSucceeds(t *testing.T) { 63 | vm, c := createConfiguration(t, errorHandlerHalt, getIDTokenReturnsSuccess) 64 | iss := "https://issuer" 65 | sub := "SUB1" 66 | 67 | jt := &jwt.Token{Claims: jwt.MapClaims(make(map[string]interface{}))} 68 | jt.Claims.(jwt.MapClaims)["iss"] = iss 69 | jt.Claims.(jwt.MapClaims)["sub"] = sub 70 | 71 | go func() { 72 | vm.assertValidate(idToken, jt, nil) 73 | vm.close() 74 | }() 75 | 76 | u, halt := authenticateUser(c, httptest.NewRecorder(), nil) 77 | 78 | if halt { 79 | t.Error("A successful authenticateUser call should not have returned halt with value true.") 80 | } 81 | 82 | if u == nil { 83 | t.Fatal("The returned user should not be nil.") 84 | } 85 | 86 | if u.Issuer != iss { 87 | t.Error("Expected user issuer", iss, ", but got", u.Issuer) 88 | } 89 | 90 | if u.ID != sub { 91 | t.Error("Expected user ID", sub, ", but got", u.ID) 92 | } 93 | 94 | thisC := u.Claims 95 | thisJT := jt.Claims.(jwt.MapClaims) 96 | if len(thisC) != len(thisJT) { 97 | t.Error("Expected number of user claims", len(thisJT), ", but got", len(u.Claims)) 98 | } 99 | 100 | vm.assertDone() 101 | } 102 | 103 | func createConfiguration(t *testing.T, eh ErrorHandlerFunc, gt GetIDTokenFunc) (*jwtTokenValidatorMock, *Configuration) { 104 | jm := newJwtTokenValidatorMock(t) 105 | c, _ := NewConfiguration(ErrorHandler(eh)) 106 | c.tokenValidator = jm 107 | c.IDTokenGetter = gt 108 | return jm, c 109 | } 110 | 111 | func getIDTokenReturnsError(r *http.Request) (string, error) { 112 | return "", errors.New("An error happened when returning ID Token.") 113 | } 114 | 115 | func getIDTokenReturnsSuccess(r *http.Request) (string, error) { 116 | return idToken, nil 117 | } 118 | 119 | func errorHandlerHalt(e error, w http.ResponseWriter, r *http.Request) bool { 120 | if e != nil { 121 | return true 122 | } 123 | return false 124 | } 125 | 126 | func errorHandlerContinue(e error, w http.ResponseWriter, r *http.Request) bool { 127 | return false 128 | } 129 | -------------------------------------------------------------------------------- /openid/pemencodemock_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import "testing" 4 | 5 | type pemEncoderMock struct { 6 | t *testing.T 7 | Calls chan Call 8 | } 9 | 10 | func newPEMEncoderMock(t *testing.T) *pemEncoderMock { 11 | return &pemEncoderMock{t, make(chan Call)} 12 | } 13 | 14 | type pemEncodeCall struct { 15 | key interface{} 16 | } 17 | 18 | type pemEncodeResponse struct { 19 | key []byte 20 | err error 21 | } 22 | 23 | func (p *pemEncoderMock) pemEncodePublicKey(key interface{}) ([]byte, error) { 24 | p.Calls <- &pemEncodeCall{key} 25 | gr := (<-p.Calls).(*pemEncodeResponse) 26 | return gr.key, gr.err 27 | } 28 | 29 | func (p *pemEncoderMock) assertPEMEncodePublicKey(key interface{}, enkey []byte, err error) { 30 | call := (<-p.Calls).(*pemEncodeCall) 31 | if call.key != key { 32 | p.t.Error("Expected pemEncode key with", key, "but was", call.key) 33 | } 34 | p.Calls <- &pemEncodeResponse{enkey, err} 35 | } 36 | 37 | func (p *pemEncoderMock) close() { 38 | close(p.Calls) 39 | } 40 | 41 | func (p *pemEncoderMock) assertDone() { 42 | if _, more := <-p.Calls; more { 43 | p.t.Fatal("Did not expect more calls.") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /openid/provider.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | // Provider represents an OpenId Identity Provider (OP) and contains 4 | // the information needed to perform validation of ID Token. 5 | // See OpenId terminology http://openid.net/specs/openid-connect-core-1_0.html#Terminology. 6 | // 7 | // The Issuer uniquely identifies an OP. This field will be used 8 | // to validate the 'iss' claim present in the ID Token. 9 | // 10 | // The CliendIDs contains the list of client IDs registered with the OP that are meant to be accepted by the service using this package. 11 | // These values are used to validate the 'aud' clain present in the ID Token. 12 | type Provider struct { 13 | Issuer string 14 | ClientIDs []string 15 | } 16 | 17 | // providers represent a collection of OPs. 18 | type providers []Provider 19 | 20 | // NewProvider returns a new instance of a Provider created with the given issuer and clientIDs. 21 | func NewProvider(issuer string, clientIDs []string) (Provider, error) { 22 | p := Provider{issuer, clientIDs} 23 | 24 | if err := p.validate(); err != nil { 25 | return Provider{}, err 26 | } 27 | 28 | return p, nil 29 | } 30 | 31 | // The GetProvidersFunc defines the function type used to retrieve the collection of allowed OP(s) along with the 32 | // respective client IDs registered with those providers that can access the backend service 33 | // using this package. 34 | // A function of this type must be provided to NewConfiguration through the option ProvidersGetter. 35 | // The given function will then be invoked for every request intercepted by the Authenticate or AuthenticateUser middleware. 36 | type GetProvidersFunc func() ([]Provider, error) 37 | 38 | func (ps providers) validate() error { 39 | if len(ps) == 0 { 40 | return &SetupError{Code: SetupErrorEmptyProviderCollection, Message: "The collection of providers must contain at least one element."} 41 | } 42 | 43 | for _, p := range ps { 44 | if err := p.validate(); err != nil { 45 | return err 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (p Provider) validate() error { 53 | if err := validateProviderIssuer(p.Issuer); err != nil { 54 | return err 55 | } 56 | 57 | if err := validateProviderClientIDs(p.ClientIDs); err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func validateProviderIssuer(iss string) error { 65 | if iss == "" { 66 | return &SetupError{Code: SetupErrorInvalidIssuer, Message: "Empty string issuer not allowed."} 67 | } 68 | 69 | // TODO: Validate that the issuer format complies with openid spec. 70 | return nil 71 | } 72 | 73 | func validateProviderClientIDs(cIDs []string) error { 74 | if len(cIDs) == 0 { 75 | return &SetupError{Code: SetupErrorInvalidClientIDs, Message: "At leat one client id must be provided."} 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /openid/provider_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import "testing" 4 | 5 | func Test_validateProviders_EmptyProviderList(t *testing.T) { 6 | var ps providers 7 | se := ps.validate() 8 | expectSetupError(t, se, SetupErrorEmptyProviderCollection) 9 | 10 | ps = make([]Provider, 0) 11 | se = ps.validate() 12 | expectSetupError(t, se, SetupErrorEmptyProviderCollection) 13 | 14 | } 15 | 16 | func Test_validateProvider_EmptyIssuer(t *testing.T) { 17 | p := Provider{} 18 | se := p.validate() 19 | expectSetupError(t, se, SetupErrorInvalidIssuer) 20 | } 21 | 22 | func Test_validateProvider_EmptyClientIDs(t *testing.T) { 23 | p := Provider{Issuer: "https://test"} 24 | se := p.validate() 25 | expectSetupError(t, se, SetupErrorInvalidClientIDs) 26 | } 27 | 28 | func Test_validateProvider_ValidProvider(t *testing.T) { 29 | p := Provider{Issuer: "https://test", ClientIDs: []string{"clientID"}} 30 | se := p.validate() 31 | 32 | if se != nil { 33 | t.Error("An error was returned but not expected", se) 34 | } 35 | } 36 | 37 | func Test_validateProviders_OneInvalidProvider(t *testing.T) { 38 | p := Provider{Issuer: "https://test", ClientIDs: []string{"clientID"}} 39 | ps := []Provider{p, {}} 40 | 41 | se := providers(ps).validate() 42 | expectSetupError(t, se, SetupErrorInvalidIssuer) 43 | } 44 | 45 | func Test_validateProviders_AllValidProviders(t *testing.T) { 46 | p := Provider{Issuer: "https://test", ClientIDs: []string{"clientID"}} 47 | ps := []Provider{p, p} 48 | 49 | se := providers(ps).validate() 50 | 51 | if se != nil { 52 | t.Error("An error was returned but not expected", se) 53 | } 54 | } 55 | 56 | func expectSetupError(t *testing.T, e error, sec SetupErrorCode) { 57 | if e == nil { 58 | t.Error("An error was expected but not returned") 59 | } 60 | 61 | if se, ok := e.(*SetupError); ok { 62 | if se.Code != sec { 63 | t.Error("Expected error code", sec, "but was", se.Code) 64 | } 65 | } else { 66 | t.Errorf("Expected error type '*SetupError' but was %T", e) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /openid/providersgettermock_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import "testing" 4 | 5 | type providersGetterMock struct { 6 | t *testing.T 7 | Calls chan Call 8 | } 9 | 10 | func newProvidersGetterMock(t *testing.T) *providersGetterMock { 11 | return &providersGetterMock{t, make(chan Call)} 12 | } 13 | 14 | type getProvidersCall struct { 15 | } 16 | 17 | type getProvidersResp struct { 18 | provs []Provider 19 | e error 20 | } 21 | 22 | func (p *providersGetterMock) getProviders() ([]Provider, error) { 23 | p.Calls <- &getProvidersCall{} 24 | gr := (<-p.Calls).(*getProvidersResp) 25 | return gr.provs, gr.e 26 | } 27 | 28 | func (p *providersGetterMock) assertGetProviders(ps []Provider, e error) { 29 | call := (<-p.Calls).(*getProvidersCall) 30 | if call == nil { 31 | p.t.Error("Expected a getProviders call but it was nil.") 32 | } 33 | 34 | p.Calls <- &getProvidersResp{ps, e} 35 | } 36 | 37 | func (p *providersGetterMock) close() { 38 | close(p.Calls) 39 | } 40 | 41 | func (p *providersGetterMock) assertDone() { 42 | if _, more := <-p.Calls; more { 43 | p.t.Fatal("Did not expect more calls.") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /openid/readidtoken.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | // GetIdTokenFunc represents the function used to provide the OIDC idToken. 9 | // It uses the provided request(r) to return the id token string(token). 10 | // If the token was not found or had a bad format this function will return an error. 11 | type GetIDTokenFunc func(r *http.Request) (token string, err error) 12 | 13 | // GetIdTokenAuthorizationHeader is the default implementation of the GetIdTokenFunc 14 | // used by this package.I looks for the idToken in the http Authorization header with 15 | // the format 'Bearer TokenString'. If found it will return 'TokenString' if not found 16 | // or the format does not match it will return an error. 17 | func getIDTokenAuthorizationHeader(r *http.Request) (t string, err error) { 18 | h := r.Header.Get("Authorization") 19 | 20 | return CheckAndSplitHeader(h) 21 | } 22 | 23 | func CheckAndSplitHeader(h string) (t string, err error) { 24 | if h == "" { 25 | return h, &ValidationError{Code: ValidationErrorAuthorizationHeaderNotFound, Message: "The 'Authorization' header was not found or was empty.", HTTPStatus: http.StatusBadRequest} 26 | } 27 | 28 | p := strings.Split(h, " ") 29 | 30 | if len(p) != 2 { 31 | return h, &ValidationError{Code: ValidationErrorAuthorizationHeaderWrongFormat, Message: "The 'Authorization' header did not have the correct format.", HTTPStatus: http.StatusBadRequest} 32 | } 33 | 34 | if p[0] != "Bearer" { 35 | return h, &ValidationError{Code: ValidationErrorAuthorizationHeaderWrongSchemeName, Message: "The 'Authorization' header scheme name was not 'Bearer'", HTTPStatus: http.StatusBadRequest} 36 | } 37 | 38 | return p[1], nil 39 | } 40 | -------------------------------------------------------------------------------- /openid/readidtoken_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | // Data used for negative tests of GetIdTokenAuthorizationHeader. 10 | var badHeaders = []struct { 11 | header string // The wrong header. 12 | errorCode ValidationErrorCode // The expected error code. 13 | httpStatus int // The expected http status code. 14 | }{ 15 | {"", ValidationErrorAuthorizationHeaderNotFound, http.StatusBadRequest}, 16 | {"token", ValidationErrorAuthorizationHeaderWrongFormat, http.StatusBadRequest}, 17 | {"token token token", ValidationErrorAuthorizationHeaderWrongFormat, http.StatusBadRequest}, 18 | {"scheme token", ValidationErrorAuthorizationHeaderWrongSchemeName, http.StatusBadRequest}, 19 | {"bearer token", ValidationErrorAuthorizationHeaderWrongSchemeName, http.StatusBadRequest}, 20 | {"Bearer token token", ValidationErrorAuthorizationHeaderWrongFormat, http.StatusBadRequest}, 21 | } 22 | 23 | // createRequest creates a request with the given string(headerContent) as the 24 | // http Authorization header and returns that request. 25 | func createRequest(headerContent string) *http.Request { 26 | r := http.Request{} 27 | r.Header = http.Header(map[string][]string{}) 28 | r.Header.Set("Authorization", headerContent) 29 | return &r 30 | } 31 | 32 | // expectError validates whether the provided error(e) has 33 | // an error code(c) 34 | func expectError(t *testing.T, e error, headerContent string, errorCode ValidationErrorCode, httpStatus int) { 35 | if ve, ok := e.(*ValidationError); ok { 36 | if ve.Code != errorCode { 37 | t.Errorf("For header %v. Expected error code %v, got %v", headerContent, errorCode, ve.Code) 38 | } 39 | if ve.HTTPStatus != httpStatus { 40 | t.Errorf("For header %v. Expected http status %v, got %v", headerContent, httpStatus, ve.HTTPStatus) 41 | } 42 | } else { 43 | t.Errorf("For header %v. Expected error type 'ValidationError', got %T", headerContent, e) 44 | } 45 | } 46 | 47 | // Tests getIdTokenAuthorizationHeader providing an Authorization header with unexpected content. 48 | func Test_getIDTokenAuthorizationHeader_WrongHeaderContent(t *testing.T) { 49 | for _, tt := range badHeaders { 50 | 51 | _, err := getIDTokenAuthorizationHeader(createRequest(tt.header)) 52 | expectError(t, err, tt.header, tt.errorCode, tt.httpStatus) 53 | } 54 | } 55 | 56 | // Tests getIdTokenAuthorizationHeader providing a request without Authorization header. 57 | func Test_getIDTokenAuthorizationHeader_NoHeader(t *testing.T) { 58 | _, err := getIDTokenAuthorizationHeader(&http.Request{}) 59 | 60 | expectError(t, err, "No Authorization Header", ValidationErrorAuthorizationHeaderNotFound, http.StatusBadRequest) 61 | } 62 | 63 | // Tests getIdTokenAuthorizationHeader providing an Authorization header with expected format. 64 | func Test_getIDTokenAuthorizationHeader_CorrectHeaderContent(t *testing.T) { 65 | et := "token" 66 | hc := fmt.Sprintf("Bearer %v", et) 67 | rt, err := getIDTokenAuthorizationHeader(createRequest(hc)) 68 | 69 | if err != nil { 70 | t.Errorf("The header content %v is valid. Unexpected error", hc) 71 | } 72 | 73 | if rt != et { 74 | t.Errorf("Expected result %v, got %v", et, rt) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /openid/signingkeyencoder.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | type pemEncodeFunc func(key interface{}) ([]byte, error) 11 | 12 | func pemEncodePublicKey(key interface{}) ([]byte, error) { 13 | mk, err := x509.MarshalPKIXPublicKey(key) 14 | if err != nil { 15 | return nil, &ValidationError{Code: ValidationErrorMarshallingKey, Message: fmt.Sprint("The jwk key could not be marshalled."), HTTPStatus: http.StatusInternalServerError, Err: err} 16 | } 17 | 18 | ed := pem.EncodeToMemory(&pem.Block{ 19 | Bytes: mk, 20 | }) 21 | 22 | return ed, nil 23 | } 24 | -------------------------------------------------------------------------------- /openid/signingkeyencoder_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "math/big" 8 | "net/http" 9 | "testing" 10 | ) 11 | 12 | func Test_pemEncodePublicKey_UsingNilKey_ReturnsMarshallingKeyError(t *testing.T) { 13 | _, err := pemEncodePublicKey(nil) 14 | 15 | if err == nil { 16 | t.Fatal("An error was expected but not returned.") 17 | } 18 | 19 | expectValidationError(t, err, ValidationErrorMarshallingKey, http.StatusInternalServerError, nil) 20 | 21 | } 22 | 23 | func Test_pemEncodePublicKey_UsingRSAPublicKey(t *testing.T) { 24 | rsaKey := &rsa.PublicKey{N: big.NewInt(9871234), E: 15} 25 | 26 | ek, err := pemEncodePublicKey(rsaKey) 27 | 28 | if err != nil { 29 | t.Error("An error was not expected but returned.") 30 | } 31 | 32 | if ek == nil { 33 | t.Error("The encoded key should not be nil.") 34 | } 35 | 36 | pBlock, rest := pem.Decode(ek) 37 | 38 | if pBlock == nil { 39 | t.Fatal("A pem block was not found in the encoded key.") 40 | } 41 | 42 | if len(rest) != 0 { 43 | t.Errorf("The encoded key was not fully pem decoded. Remaining buffer len %v.", len(rest)) 44 | } 45 | 46 | pub, err := x509.ParsePKIXPublicKey(pBlock.Bytes) 47 | 48 | if err != nil { 49 | t.Errorf("Parsing the key as DER public key returned the error %v.", err) 50 | } 51 | 52 | if pub == nil { 53 | t.Fatal("The key could not be parsed as a DER public key.") 54 | } 55 | 56 | if rpk, ok := pub.(*rsa.PublicKey); ok { 57 | rn := rpk.N.Int64() 58 | en := rsaKey.N.Int64() 59 | if en != rn { 60 | t.Error("Expected N", en, "but got", rn) 61 | } 62 | if rpk.E != rsaKey.E { 63 | t.Error("Expected E", rsaKey.E, "but got", rpk.E) 64 | } 65 | } else { 66 | t.Errorf("Expected public key type '*rsa.PublicKey' but got %T.", pub) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /openid/signingkeygettermock_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import "testing" 4 | 5 | type signingKeyGetterMock struct { 6 | t *testing.T 7 | Calls chan Call 8 | } 9 | 10 | func newSigningKeyGetterMock(t *testing.T) *signingKeyGetterMock { 11 | return &signingKeyGetterMock{t, make(chan Call)} 12 | } 13 | 14 | type getSigningKeyCall struct { 15 | iss string 16 | keyID string 17 | } 18 | 19 | type getSigningKeyResp struct { 20 | key []byte 21 | err error 22 | } 23 | 24 | type flushCachedSigningKeysCall struct { 25 | iss string 26 | } 27 | 28 | type flushCachedSigningKeysResp struct { 29 | err error 30 | } 31 | 32 | func (s *signingKeyGetterMock) getSigningKey(iss string, keyID string) (interface{}, error) { 33 | s.Calls <- &getSigningKeyCall{iss, keyID} 34 | sr := (<-s.Calls).(*getSigningKeyResp) 35 | return sr.key, sr.err 36 | } 37 | 38 | func (s *signingKeyGetterMock) flushCachedSigningKeys(iss string) error { 39 | s.Calls <- &flushCachedSigningKeysCall{iss} 40 | sr := (<-s.Calls).(*flushCachedSigningKeysResp) 41 | return sr.err 42 | } 43 | 44 | func (s *signingKeyGetterMock) assertGetSigningKey(iss string, keyID string, key []byte, err error) { 45 | call := (<-s.Calls).(*getSigningKeyCall) 46 | if iss != anything && call.iss != iss { 47 | s.t.Error("Expected getSigningKey with issuer", iss, "but was", call.iss) 48 | } 49 | if keyID != anything && call.keyID != keyID { 50 | s.t.Error("Expected getSigningKey with key ID", keyID, "but was", call.keyID) 51 | } 52 | s.Calls <- &getSigningKeyResp{key, err} 53 | } 54 | 55 | func (s *signingKeyGetterMock) assertFlushCachedSigningKeys(iss string, err error) { 56 | call := (<-s.Calls).(*flushCachedSigningKeysCall) 57 | if iss != anything && call.iss != iss { 58 | s.t.Error("Expected flushCachedSigningKeys with issuer", iss, "but was", call.iss) 59 | } 60 | 61 | s.Calls <- &flushCachedSigningKeysResp{err} 62 | } 63 | 64 | func (s *signingKeyGetterMock) close() { 65 | close(s.Calls) 66 | } 67 | 68 | func (s *signingKeyGetterMock) assertDone() { 69 | if _, more := <-s.Calls; more { 70 | s.t.Fatal("Did not expect more calls.") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /openid/signingkeyprovider.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | "fmt" 7 | "net/http" 8 | "sync" 9 | ) 10 | 11 | var lock = sync.RWMutex{} 12 | 13 | type signingKeyGetter interface { 14 | flushCachedSigningKeys(issuer string) error 15 | getSigningKey(issuer string, kid string) (interface{}, error) 16 | } 17 | 18 | type signingKeyProvider struct { 19 | keySetGetter signingKeySetGetter 20 | jwksMap map[string][]signingKey 21 | } 22 | 23 | func newSigningKeyProvider(kg signingKeySetGetter) *signingKeyProvider { 24 | keyMap := make(map[string][]signingKey) 25 | return &signingKeyProvider{kg, keyMap} 26 | } 27 | 28 | func (s *signingKeyProvider) flushCachedSigningKeys(issuer string) error { 29 | lock.Lock() 30 | defer lock.Unlock() 31 | delete(s.jwksMap, issuer) 32 | return nil 33 | } 34 | 35 | func (s *signingKeyProvider) refreshSigningKeys(issuer string) error { 36 | skeys, err := s.keySetGetter.getSigningKeySet(issuer) 37 | 38 | if err != nil { 39 | return err 40 | } 41 | 42 | lock.Lock() 43 | s.jwksMap[issuer] = skeys 44 | lock.Unlock() 45 | return nil 46 | } 47 | 48 | func parsePublicKey(data []byte) (interface{}, error) { 49 | input := data 50 | block, _ := pem.Decode(data) 51 | if block != nil { 52 | input = block.Bytes 53 | } 54 | var pub interface{} 55 | var err error 56 | pub, err = x509.ParsePKIXPublicKey(input) 57 | if err != nil { 58 | cert, err0 := x509.ParseCertificate(input) 59 | if err0 != nil { 60 | return nil, err0 61 | } 62 | pub = cert.PublicKey 63 | err = nil 64 | } 65 | return pub, err 66 | } 67 | 68 | func (s *signingKeyProvider) getSigningKey(issuer string, kid string) (interface{}, error) { 69 | lock.RLock() 70 | sk := findKey(s.jwksMap, issuer, kid) 71 | lock.RUnlock() 72 | 73 | if sk != nil { 74 | parsed, pErr := parsePublicKey(sk) 75 | if pErr != nil { 76 | return sk, nil 77 | } 78 | return parsed, nil 79 | } 80 | 81 | err := s.refreshSigningKeys(issuer) 82 | 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | lock.RLock() 88 | sk = findKey(s.jwksMap, issuer, kid) 89 | lock.RUnlock() 90 | 91 | if sk == nil { 92 | return nil, &ValidationError{Code: ValidationErrorKidNotFound, Message: fmt.Sprintf("The jwk set retrieved for the issuer %v does not contain a key identifier %v.", issuer, kid), HTTPStatus: http.StatusUnauthorized} 93 | } 94 | 95 | parsed, pErr := parsePublicKey(sk) 96 | if pErr != nil { 97 | return sk, nil 98 | } 99 | 100 | return parsed, nil 101 | } 102 | 103 | func findKey(km map[string][]signingKey, issuer string, kid string) []byte { 104 | 105 | if skSet, ok := km[issuer]; ok { 106 | if kid == "" { 107 | return skSet[0].key 108 | } else { 109 | for _, sk := range skSet { 110 | if sk.keyID == kid { 111 | return sk.key 112 | } 113 | } 114 | } 115 | } 116 | 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /openid/signingkeyprovider_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func Test_getSigningKey_WhenKeyIsCached(t *testing.T) { 9 | _, keyCache := createSigningKeyProvider(t) 10 | 11 | iss := "issuer" 12 | kid := "kid1" 13 | key := "signingKey" 14 | keyCache.jwksMap[iss] = []signingKey{{keyID: kid, key: []byte(key)}} 15 | 16 | expectKey(t, keyCache, iss, kid, key) 17 | } 18 | 19 | func Test_getSigningKey_WhenKeyIsNotCached_WhenProviderReturnsKey(t *testing.T) { 20 | keyGetter, keyCache := createSigningKeyProvider(t) 21 | 22 | iss := "issuer" 23 | kid := "kid1" 24 | key := "signingKey" 25 | 26 | go func() { 27 | keyGetter.assertGetSigningKeySet(iss, []signingKey{{keyID: kid, key: []byte(key)}}, nil) 28 | keyGetter.close() 29 | }() 30 | 31 | expectKey(t, keyCache, iss, kid, key) 32 | 33 | // Validate that the key is cached 34 | expectCachedKid(t, keyCache, iss, kid, key) 35 | 36 | keyGetter.assertDone() 37 | } 38 | 39 | func Test_getSigningKey_WhenProviderReturnsError(t *testing.T) { 40 | keyGetter, keyCache := createSigningKeyProvider(t) 41 | 42 | iss := "issuer" 43 | kid := "kid1" 44 | ee := &ValidationError{Code: ValidationErrorGetJwksFailure, HTTPStatus: http.StatusUnauthorized} 45 | 46 | go func() { 47 | keyGetter.assertGetSigningKeySet(iss, nil, ee) 48 | keyGetter.close() 49 | }() 50 | 51 | rk, re := keyCache.getSigningKey(iss, kid) 52 | 53 | expectValidationError(t, re, ee.Code, ee.HTTPStatus, nil) 54 | 55 | if rk != nil { 56 | t.Error("A key was returned but not expected") 57 | } 58 | 59 | cachedKeys := keyCache.jwksMap[iss] 60 | if len(cachedKeys) != 0 { 61 | t.Fatal("There shouldnt be cached keys for the targeted issuer.") 62 | } 63 | 64 | keyGetter.assertDone() 65 | } 66 | 67 | func Test_getSigningKey_WhenKeyIsNotFound(t *testing.T) { 68 | keyGetter, keyCache := createSigningKeyProvider(t) 69 | 70 | iss := "issuer" 71 | kid := "kid1" 72 | tkid := "kid2" 73 | key := "signingKey" 74 | 75 | go func() { 76 | keyGetter.assertGetSigningKeySet(iss, []signingKey{{keyID: kid, key: []byte(key)}}, nil) 77 | keyGetter.close() 78 | }() 79 | 80 | rk, re := keyCache.getSigningKey(iss, tkid) 81 | 82 | expectValidationError(t, re, ValidationErrorKidNotFound, http.StatusUnauthorized, nil) 83 | 84 | if rk != nil { 85 | t.Error("A key was returned but not expected") 86 | } 87 | 88 | expectCachedKid(t, keyCache, iss, kid, key) 89 | 90 | keyGetter.assertDone() 91 | } 92 | 93 | func Test_flushCachedSigningKeys_FlushedKeysAreDeleted(t *testing.T) { 94 | _, keyCache := createSigningKeyProvider(t) 95 | 96 | iss := "issuer" 97 | iss2 := "issuer2" 98 | kid := "kid1" 99 | key := "signingKey" 100 | keyCache.jwksMap[iss] = []signingKey{{keyID: kid, key: []byte(key)}} 101 | keyCache.jwksMap[iss2] = []signingKey{{keyID: kid, key: []byte(key)}} 102 | 103 | keyCache.flushCachedSigningKeys(iss2) 104 | 105 | dk := keyCache.jwksMap[iss2] 106 | 107 | if dk != nil { 108 | t.Error("Flushed keys should not be in the cache.") 109 | } 110 | 111 | expectCachedKid(t, keyCache, iss, kid, key) 112 | } 113 | 114 | func Test_flushCachedSigningKey_RetrieveFlushedKey(t *testing.T) { 115 | keyGetter, keyCache := createSigningKeyProvider(t) 116 | 117 | iss := "issuer" 118 | kid := "kid1" 119 | key := "signingKey" 120 | 121 | go func() { 122 | keyGetter.assertGetSigningKeySet(iss, []signingKey{{keyID: kid, key: []byte(key)}}, nil) 123 | keyGetter.assertGetSigningKeySet(iss, []signingKey{{keyID: kid, key: []byte(key)}}, nil) 124 | 125 | keyGetter.close() 126 | }() 127 | 128 | // Get the signing key not yet cached will cache it. 129 | expectKey(t, keyCache, iss, kid, key) 130 | 131 | // Flush the signing keys for the given provider. 132 | keyCache.flushCachedSigningKeys(iss) 133 | 134 | // Get the signing key will once again call the provider and cache the keys. 135 | 136 | expectKey(t, keyCache, iss, kid, key) 137 | 138 | // Validate that the key is cached 139 | expectCachedKid(t, keyCache, iss, kid, key) 140 | 141 | keyGetter.assertDone() 142 | } 143 | 144 | func expectCachedKid(t *testing.T, keyProv *signingKeyProvider, iss string, kid string, key string) { 145 | 146 | cachedKeys := keyProv.jwksMap[iss] 147 | if len(cachedKeys) == 0 { 148 | t.Fatal("The keys were not cached as expected.") 149 | } 150 | 151 | foundKid := false 152 | for _, cachedKey := range cachedKeys { 153 | if cachedKey.keyID == kid { 154 | foundKid = true 155 | if keyStr := string(cachedKey.key); keyStr != key { 156 | t.Error("Expected key", key, "but got", keyStr) 157 | } 158 | 159 | continue 160 | } 161 | } 162 | 163 | if !foundKid { 164 | t.Error("A key with key id", kid, "was not found.") 165 | } 166 | } 167 | 168 | func expectKey(t *testing.T, c signingKeyGetter, iss string, kid string, key string) { 169 | 170 | sk, re := c.getSigningKey(iss, kid) 171 | 172 | if re != nil { 173 | t.Error("An error was returned but not expected.") 174 | } 175 | 176 | if sk == nil { 177 | t.Fatal("The returned signing key should not be nil.") 178 | } 179 | 180 | // Commenting out because we are using an interface now, not a string 181 | // keyStr := string(sk) 182 | 183 | // if keyStr != key { 184 | // t.Error("Expected key", key, "but got", keyStr) 185 | // } 186 | } 187 | 188 | func createSigningKeyProvider(t *testing.T) (*signingKeySetGetterMock, *signingKeyProvider) { 189 | mock := newSigningKeySetGetterMock(t) 190 | return mock, newSigningKeyProvider(mock) 191 | } 192 | -------------------------------------------------------------------------------- /openid/signingkeysetgettermock_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import "testing" 4 | 5 | type signingKeySetGetterMock struct { 6 | t *testing.T 7 | Calls chan Call 8 | } 9 | 10 | func newSigningKeySetGetterMock(t *testing.T) *signingKeySetGetterMock { 11 | return &signingKeySetGetterMock{t, make(chan Call)} 12 | } 13 | 14 | type getSigningKeySetCall struct { 15 | iss string 16 | } 17 | 18 | type getSigningKeySetResponse struct { 19 | keys []signingKey 20 | err error 21 | } 22 | 23 | func (c *signingKeySetGetterMock) getSigningKeySet(iss string) ([]signingKey, error) { 24 | c.Calls <- &getSigningKeySetCall{iss} 25 | sr := (<-c.Calls).(*getSigningKeySetResponse) 26 | return sr.keys, sr.err 27 | } 28 | 29 | func (c *signingKeySetGetterMock) assertGetSigningKeySet(iss string, keys []signingKey, err error) { 30 | call := (<-c.Calls).(*getSigningKeySetCall) 31 | if iss != anything && call.iss != iss { 32 | c.t.Error("Expected getSigningKeySet with issuer", iss, "but was", call.iss) 33 | } 34 | c.Calls <- &getSigningKeySetResponse{keys, err} 35 | } 36 | 37 | func (c *signingKeySetGetterMock) close() { 38 | close(c.Calls) 39 | } 40 | 41 | func (c *signingKeySetGetterMock) assertDone() { 42 | if _, more := <-c.Calls; more { 43 | c.t.Fatal("Did not expect more calls.") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /openid/signingkeysetprovider.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type signingKeySetGetter interface { 9 | getSigningKeySet(issuer string) ([]signingKey, error) 10 | } 11 | 12 | type signingKeySetProvider struct { 13 | configGetter configurationGetter 14 | jwksGetter jwksGetter 15 | keyEncoder pemEncodeFunc 16 | } 17 | 18 | type signingKey struct { 19 | keyID string 20 | key []byte 21 | } 22 | 23 | func newSigningKeySetProvider(cg configurationGetter, jg jwksGetter, ke pemEncodeFunc) *signingKeySetProvider { 24 | return &signingKeySetProvider{cg, jg, ke} 25 | } 26 | 27 | func (signProv *signingKeySetProvider) getSigningKeySet(iss string) ([]signingKey, error) { 28 | conf, err := signProv.configGetter.getConfiguration(iss) 29 | 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | jwks, err := signProv.jwksGetter.getJwkSet(conf.JwksUri) 35 | 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | if len(jwks.Keys) == 0 { 41 | return nil, &ValidationError{Code: ValidationErrorEmptyJwk, Message: fmt.Sprintf("The jwk set retrieved for the issuer %v does not contain any key.", iss), HTTPStatus: http.StatusUnauthorized} 42 | } 43 | 44 | sk := make([]signingKey, len(jwks.Keys)) 45 | 46 | for i, k := range jwks.Keys { 47 | ek, err := signProv.keyEncoder(k.Key) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | sk[i] = signingKey{k.KeyID, ek} 53 | } 54 | 55 | return sk, nil 56 | } 57 | -------------------------------------------------------------------------------- /openid/signingkeysetprovider_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/go-jose/go-jose/v3" 9 | ) 10 | 11 | func Test_getsigningKeySet_WhenGetConfigurationReturnsError(t *testing.T) { 12 | configGetter, _, _, skProv := createSigningKeySetProvider(t) 13 | 14 | ee := &ValidationError{Code: ValidationErrorGetOpenIdConfigurationFailure, HTTPStatus: http.StatusUnauthorized} 15 | 16 | go func() { 17 | configGetter.assertGetConfiguration(anything, configuration{}, ee) 18 | configGetter.close() 19 | }() 20 | 21 | sk, re := skProv.getSigningKeySet(anything) 22 | 23 | expectValidationError(t, re, ee.Code, ee.HTTPStatus, nil) 24 | 25 | if sk != nil { 26 | t.Error("The returned signing keys should be nil") 27 | } 28 | 29 | configGetter.assertDone() 30 | } 31 | 32 | func Test_getsigningKeySet_WhenGetJwksReturnsError(t *testing.T) { 33 | configGetter, jwksGetter, _, skProv := createSigningKeySetProvider(t) 34 | 35 | ee := &ValidationError{Code: ValidationErrorGetJwksFailure, HTTPStatus: http.StatusUnauthorized} 36 | 37 | go func() { 38 | configGetter.assertGetConfiguration(anything, configuration{}, nil) 39 | configGetter.close() 40 | jwksGetter.assertGetJwks(anything, jose.JSONWebKeySet{}, ee) 41 | jwksGetter.close() 42 | }() 43 | 44 | sk, re := skProv.getSigningKeySet(anything) 45 | 46 | expectValidationError(t, re, ee.Code, ee.HTTPStatus, nil) 47 | 48 | if sk != nil { 49 | t.Error("The returned signing keys should be nil") 50 | } 51 | 52 | configGetter.assertDone() 53 | jwksGetter.assertDone() 54 | } 55 | 56 | func Test_getsigningKeySet_WhenJwkSetIsEmpty(t *testing.T) { 57 | configGetter, jwksGetter, _, skProv := createSigningKeySetProvider(t) 58 | 59 | ee := &ValidationError{Code: ValidationErrorEmptyJwk, HTTPStatus: http.StatusUnauthorized} 60 | 61 | go func() { 62 | configGetter.assertGetConfiguration(anything, configuration{}, nil) 63 | configGetter.close() 64 | jwksGetter.assertGetJwks(anything, jose.JSONWebKeySet{}, nil) 65 | jwksGetter.close() 66 | }() 67 | 68 | sk, re := skProv.getSigningKeySet(anything) 69 | 70 | expectValidationError(t, re, ee.Code, ee.HTTPStatus, nil) 71 | 72 | if sk != nil { 73 | t.Error("The returned signing keys should be nil") 74 | } 75 | 76 | configGetter.assertDone() 77 | jwksGetter.assertDone() 78 | } 79 | 80 | func Test_getsigningKeySet_WhenKeyEncodingReturnsError(t *testing.T) { 81 | configGetter, jwksGetter, pemEncoder, skProv := createSigningKeySetProvider(t) 82 | 83 | ee := &ValidationError{Code: ValidationErrorMarshallingKey, HTTPStatus: http.StatusInternalServerError} 84 | ejwks := jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: nil}}} 85 | 86 | go func() { 87 | configGetter.assertGetConfiguration(anything, configuration{}, nil) 88 | configGetter.close() 89 | jwksGetter.assertGetJwks(anything, ejwks, nil) 90 | jwksGetter.close() 91 | pemEncoder.assertPEMEncodePublicKey(nil, nil, ee) 92 | pemEncoder.close() 93 | }() 94 | 95 | sk, re := skProv.getSigningKeySet(anything) 96 | 97 | expectValidationError(t, re, ee.Code, ee.HTTPStatus, nil) 98 | 99 | if sk != nil { 100 | t.Error("The returned signing keys should be nil") 101 | } 102 | 103 | configGetter.assertDone() 104 | jwksGetter.assertDone() 105 | pemEncoder.assertDone() 106 | } 107 | 108 | func Test_getsigningKeySet_WhenKeyEncodingReturnsSuccess(t *testing.T) { 109 | configGetter, jwksGetter, pemEncoder, skProv := createSigningKeySetProvider(t) 110 | 111 | keys := make([]jose.JSONWebKey, 2) 112 | encryptedKeys := make([]signingKey, 2) 113 | 114 | for i := 0; i < cap(keys); i = i + 1 { 115 | keys[i] = jose.JSONWebKey{KeyID: fmt.Sprintf("%v", i), Key: i} 116 | encryptedKeys[i] = signingKey{keyID: fmt.Sprintf("%v", i), key: []byte(fmt.Sprintf("%v", i))} 117 | } 118 | 119 | ejwks := jose.JSONWebKeySet{Keys: keys} 120 | go func() { 121 | configGetter.assertGetConfiguration(anything, configuration{}, nil) 122 | jwksGetter.assertGetJwks(anything, ejwks, nil) 123 | for i, encryptedKey := range encryptedKeys { 124 | pemEncoder.assertPEMEncodePublicKey(keys[i].Key, encryptedKey.key, nil) 125 | } 126 | configGetter.close() 127 | jwksGetter.close() 128 | pemEncoder.close() 129 | }() 130 | 131 | sk, re := skProv.getSigningKeySet(anything) 132 | 133 | if re != nil { 134 | t.Error("An error was returned but not expected.") 135 | } 136 | 137 | if sk == nil { 138 | t.Fatal("The returned signing keys should be not nil") 139 | } 140 | 141 | if len(sk) != len(encryptedKeys) { 142 | t.Error("Returned", len(sk), "encrypted keys, but expected", len(encryptedKeys)) 143 | } 144 | 145 | for i, encryptedKey := range encryptedKeys { 146 | if encryptedKey.keyID != sk[i].keyID { 147 | t.Error("Key at", i, "should have keyID", encryptedKey.keyID, "but was", sk[i].keyID) 148 | } 149 | if string(encryptedKey.key) != string(sk[i].key) { 150 | t.Error("Key at", i, "should be", encryptedKey.key, "but was", sk[i].key) 151 | } 152 | } 153 | 154 | configGetter.assertDone() 155 | jwksGetter.assertDone() 156 | pemEncoder.assertDone() 157 | } 158 | 159 | func createSigningKeySetProvider(t *testing.T) (*configurationGetterMock, *jwksGetterMock, *pemEncoderMock, signingKeySetProvider) { 160 | configGetter := newConfigurationGetterMock(t) 161 | jwksGetter := newJwksGetterMock(t) 162 | pemEncoder := newPEMEncoderMock(t) 163 | 164 | skProv := signingKeySetProvider{configGetter: configGetter, jwksGetter: jwksGetter, keyEncoder: pemEncoder.pemEncodePublicKey} 165 | return configGetter, jwksGetter, pemEncoder, skProv 166 | } 167 | -------------------------------------------------------------------------------- /openid/user.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/golang-jwt/jwt/v4" 7 | ) 8 | 9 | // User represents the authenticated user encapsulating information obtained from the validated ID token. 10 | // 11 | // The Issuer contains the value from the 'iss' claim found in the ID Token. 12 | // 13 | // The ID contains the value of the 'sub' claim found in the ID Token. 14 | // 15 | // The Claims contains all the claims present found in the ID Token 16 | type User struct { 17 | Issuer string 18 | ID string 19 | Claims map[string]interface{} 20 | } 21 | 22 | func newUser(t *jwt.Token) (*User, error) { 23 | if t == nil { 24 | return nil, &ValidationError{Code: ValidationErrorIdTokenEmpty, Message: "The token provided to created a user was nil.", HTTPStatus: http.StatusUnauthorized} 25 | } 26 | 27 | iss := getIssuer(t).(string) 28 | 29 | if iss == "" { 30 | return nil, &ValidationError{Code: ValidationErrorInvalidIssuer, Message: "The token provided to created a user did not contain a valid 'iss' claim", HTTPStatus: http.StatusInternalServerError} 31 | } 32 | 33 | sub := getSubject(t).(string) 34 | 35 | if sub == "" { 36 | return nil, &ValidationError{Code: ValidationErrorInvalidSubject, Message: "The token provided to created a user did not contain a valid 'sub' claim.", HTTPStatus: http.StatusInternalServerError} 37 | 38 | } 39 | 40 | u := new(User) 41 | u.Issuer = iss 42 | u.ID = sub 43 | u.Claims = t.Claims.(jwt.MapClaims) 44 | return u, nil 45 | } 46 | -------------------------------------------------------------------------------- /openid/userhandler.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import "net/http" 4 | 5 | // The UserHandler represents a handler to be registered by the middleware AuthenticateUser. 6 | // This handler allows the AuthenticateUser middleware to forward information about the the authenticated user to 7 | // the rest of the application service. 8 | // 9 | // ServeHTTPWithUser is similar to the http.ServeHTTP function. It contains an additional paramater *User, 10 | // which is used by the AuthenticateUser middleware to pass information about the authenticated user. 11 | type UserHandler interface { 12 | ServeHTTPWithUser(*User, http.ResponseWriter, *http.Request) 13 | } 14 | 15 | // The UserHandlerFunc is an adapter to allow the use of functions as UserHandler. 16 | // This is similar to using http.HandlerFunc as http.Handler 17 | type UserHandlerFunc func(*User, http.ResponseWriter, *http.Request) 18 | 19 | // ServeHttpWithUser calls f(u, w, r) 20 | func (f UserHandlerFunc) ServeHTTPWithUser(u *User, w http.ResponseWriter, r *http.Request) { 21 | f(u, w, r) 22 | } 23 | --------------------------------------------------------------------------------