├── .gitignore ├── .travis.yml ├── Gopkg.toml ├── LICENSE ├── README.md ├── alice-example ├── LICENSE ├── README.md └── main.go ├── gorilla-example ├── LICENSE ├── README.md └── main.go └── openid ├── LICENSE ├── README.md ├── configuration.go ├── configurationprovider.go ├── configurationprovider_test.go ├── doc.go ├── errors.go ├── example_test.go ├── idtokenvalidator.go ├── idtokenvalidator_test.go ├── integration_test.go ├── jwksprovider.go ├── jwksprovider_test.go ├── middleware.go ├── middleware_test.go ├── mocks_test.go ├── provider.go ├── provider_test.go ├── readidtoken.go ├── readidtoken_test.go ├── signingkeyencoder.go ├── signingkeyencoder_test.go ├── signingkeyprovider.go ├── signingkeyprovider_test.go ├── signingkeysetprovider.go ├── signingkeysetprovider_test.go ├── user.go └── userhandler.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.go~ 2 | *.exe 3 | *.go# 4 | /vendor 5 | Gopkg.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | env: 4 | - DEP_VERSION="0.4.1" 5 | 6 | go: 7 | - "1.9.x" 8 | - tip 9 | 10 | before_install: 11 | # Download the binary to bin folder in $GOPATH 12 | - curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -o $GOPATH/bin/dep 13 | # Make the binary executable 14 | - chmod +x $GOPATH/bin/dep 15 | 16 | install: 17 | - dep ensure 18 | 19 | script: 20 | - go vet ./... 21 | - go test -v ./openid 22 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/dgrijalva/jwt-go" 30 | version = "3.2.0" 31 | 32 | [[constraint]] 33 | name = "github.com/gorilla/context" 34 | version = "1.1.0" 35 | 36 | [[constraint]] 37 | branch = "master" 38 | name = "github.com/justinas/alice" 39 | 40 | [[constraint]] 41 | name = "gopkg.in/square/go-jose.v2" 42 | version = "2.1.4" 43 | 44 | [prune] 45 | go-tests = true 46 | unused-packages = true 47 | -------------------------------------------------------------------------------- /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 | 8 | See [openid package README](/openid/README.md) -------------------------------------------------------------------------------- /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/emanoelxavier/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 | -------------------------------------------------------------------------------- /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 across 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/emanoelxavier/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 | 6 | ## Summary 7 | 8 | A Go package that implements web service middlewares for authenticating identities represented by OpenID Connect (OIDC) ID Tokens. 9 | 10 | "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) 11 | 12 | ## Installation 13 | 14 | go get github.com/emanoelxavier/openid2go/openid 15 | 16 | ## Example 17 | 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. 18 | 19 | ```go 20 | import ( 21 | "fmt" 22 | "net/http" 23 | 24 | "github.com/emanoelxavier/openid2go/openid" 25 | ) 26 | 27 | func AuthenticatedHandler(w http.ResponseWriter, r *http.Request) { 28 | fmt.Fprintln(w, "The user was authenticated!") 29 | } 30 | 31 | func AuthenticatedHandlerWithUser(u *openid.User, w http.ResponseWriter, r *http.Request) { 32 | fmt.Fprintf(w, "The user was authenticated! The token was issued by %v and the user is %+v.", u.Issuer, u) 33 | } 34 | 35 | func Example() { 36 | configuration, err := openid.NewConfiguration(openid.ProvidersGetter(myGetProviders)) 37 | 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | http.Handle("/user", openid.AuthenticateUser(configuration, openid.UserHandlerFunc(AuthenticatedHandlerWithUser))) 43 | http.Handle("/authn", openid.Authenticate(configuration, http.HandlerFunc(AuthenticatedHandler))) 44 | 45 | http.ListenAndServe(":5100", nil) 46 | } 47 | 48 | func myGetProviders() ([]openid.Provider, error) { 49 | provider, err := openid.NewProvider("https://providerissuer", []string{"myClientID"}) 50 | 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return []openid.Provider{provider}, nil 56 | } 57 | ``` 58 | This example is also available in the documentation of this package, for more details see [GoDoc](https://godoc.org/github.com/emanoelxavier/openid2go/openid). 59 | 60 | Additional examples: 61 | * [Alice Example](../alice-example) 62 | * [Gorilla Example](../gorilla-example) 63 | 64 | 65 | ## Tests 66 | 67 | #### Unit Tests 68 | ```sh 69 | go test github.com/emanoelxavier/openid2go/openid 70 | ``` 71 | 72 | #### Integration Tests 73 | 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: 74 | 75 | ```sh 76 | go test -tags integration github.com/emanoelxavier/openid2go/openid -issuer=[issuer] -clientID=[clientID] -idToken=[idToken] 77 | ``` 78 | 79 | Replace [issuer], [clientID] and [idToken] with the information from an identity provider of your choice. 80 | 81 | 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: 82 | 83 | ```sh 84 | go test -tags integration github.com/emanoelxavier/openid2go/openid -issuer=https://accounts.google.com -clientID=407408718192.apps.googleusercontent.com -idToken=copiedIDToken 85 | ``` 86 | 87 | ## Contributing 88 | 89 | 1. Open an issue if found a bug or have a functional request. 90 | 2. Disccuss. 91 | 3. Branch off, write the fix with test(s) and commit attaching to the issue. 92 | 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/configurationprovider.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | const wellKnownOpenIDConfiguration = "/.well-known/openid-configuration" 11 | 12 | type configurationGetter interface { 13 | get(r *http.Request, url string) (configuration, error) 14 | } 15 | 16 | type configurationDecoder interface { 17 | decode(io.Reader) (configuration, error) 18 | } 19 | 20 | type httpGetter interface { 21 | get(r *http.Request, url string) (*http.Response, error) 22 | } 23 | 24 | func (f HTTPGetFunc) get(r *http.Request, url string) (*http.Response, error) { 25 | return f(r, url) 26 | } 27 | 28 | type httpConfigurationProvider struct { 29 | getter httpGetter 30 | decoder configurationDecoder 31 | } 32 | 33 | func newHTTPConfigurationProvider(gc HTTPGetFunc, dc configurationDecoder) *httpConfigurationProvider { 34 | return &httpConfigurationProvider{gc, dc} 35 | } 36 | 37 | func (httpProv *httpConfigurationProvider) get(r *http.Request, issuer string) (configuration, error) { 38 | // Workaround for tokens issued by google 39 | if issuer == "accounts.google.com" { 40 | issuer = "https://" + issuer 41 | } 42 | configurationURI := issuer + wellKnownOpenIDConfiguration 43 | var config configuration 44 | resp, err := httpProv.getter.get(r, configurationURI) 45 | if err != nil { 46 | return config, &ValidationError{ 47 | Code: ValidationErrorGetOpenIdConfigurationFailure, 48 | Message: fmt.Sprintf("Failure while contacting the configuration endpoint %v.", configurationURI), 49 | Err: err, 50 | HTTPStatus: http.StatusUnauthorized, 51 | } 52 | } 53 | 54 | defer resp.Body.Close() 55 | 56 | if config, err = httpProv.decoder.decode(resp.Body); err != nil { 57 | return config, &ValidationError{ 58 | Code: ValidationErrorDecodeOpenIdConfigurationFailure, 59 | Message: fmt.Sprintf("Failure while decoding the configuration retrived from endpoint %v.", configurationURI), 60 | Err: err, 61 | HTTPStatus: http.StatusUnauthorized, 62 | } 63 | } 64 | 65 | return config, nil 66 | } 67 | 68 | func jsonDecodeResponse(r io.Reader, v interface{}) error { 69 | return json.NewDecoder(r).Decode(v) 70 | } 71 | 72 | type jsonConfigurationDecoder struct { 73 | } 74 | 75 | func (d *jsonConfigurationDecoder) decode(r io.Reader) (configuration, error) { 76 | var config configuration 77 | err := jsonDecodeResponse(r, &config) 78 | 79 | return config, err 80 | } 81 | -------------------------------------------------------------------------------- /openid/configurationprovider_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | 14 | "github.com/stretchr/testify/mock" 15 | ) 16 | 17 | type testBody struct { 18 | io.Reader 19 | } 20 | 21 | func (testBody) Close() error { return nil } 22 | 23 | func TestConfigurationProvider_Get_UsesCorrectUrlAndRequest(t *testing.T) { 24 | httpGetter := &mockHTTPGetter{} 25 | configurationProvider := httpConfigurationProvider{getter: httpGetter} 26 | req := httptest.NewRequest(http.MethodGet, "/", nil) 27 | 28 | issuer := "https://test" 29 | configSuffix := "/.well-known/openid-configuration" 30 | httpGetter.On("get", req, issuer+configSuffix).Return(nil, errors.New("Read configuration error")) 31 | 32 | _, e := configurationProvider.get(req, issuer) 33 | 34 | if e == nil { 35 | t.Error("An error was expected but not returned") 36 | } 37 | 38 | httpGetter.AssertExpectations(t) 39 | } 40 | 41 | func TestConfigurationProvider_Get_WhenGetReturnsError(t *testing.T) { 42 | httpGetter := &mockHTTPGetter{} 43 | configurationProvider := httpConfigurationProvider{getter: httpGetter} 44 | 45 | readError := errors.New("Read configuration error") 46 | httpGetter.On("get", (*http.Request)(nil), mock.Anything).Return(nil, readError) 47 | 48 | _, e := configurationProvider.get(nil, "issuer") 49 | 50 | expectValidationError(t, e, ValidationErrorGetOpenIdConfigurationFailure, http.StatusUnauthorized, readError) 51 | 52 | httpGetter.AssertExpectations(t) 53 | } 54 | 55 | func TestConfigurationProvider_Get_WhenGetSucceeds(t *testing.T) { 56 | httpGetter := &mockHTTPGetter{} 57 | configDecoder := &mockConfigurationDecoder{} 58 | configurationProvider := httpConfigurationProvider{httpGetter, configDecoder} 59 | 60 | respBody := "openid configuration" 61 | resp := &http.Response{Body: testBody{bytes.NewBufferString(respBody)}} 62 | 63 | httpGetter.On("get", (*http.Request)(nil), mock.Anything).Return(resp, nil) 64 | configDecoder.On("decode", mock.MatchedBy(ioReaderMatcher(t, respBody))).Return(configuration{}, nil) 65 | 66 | _, e := configurationProvider.get(nil, mock.Anything) 67 | 68 | if e != nil { 69 | t.Error("An error was returned but not expected", e) 70 | } 71 | 72 | httpGetter.AssertExpectations(t) 73 | configDecoder.AssertExpectations(t) 74 | } 75 | 76 | func TestConfigurationProvider_Get_WhenDecodeResponseReturnsError(t *testing.T) { 77 | httpGetter := &mockHTTPGetter{} 78 | configDecoder := &mockConfigurationDecoder{} 79 | 80 | configurationProvider := httpConfigurationProvider{httpGetter, configDecoder} 81 | decodeError := errors.New("Decode configuration error") 82 | respBody := "openid configuration" 83 | resp := &http.Response{Body: testBody{bytes.NewBufferString(respBody)}} 84 | httpGetter.On("get", (*http.Request)(nil), mock.Anything).Return(resp, nil) 85 | 86 | configDecoder.On("decode", mock.MatchedBy(ioReaderMatcher(t, respBody))).Return(configuration{}, decodeError) 87 | _, e := configurationProvider.get(nil, mock.Anything) 88 | 89 | expectValidationError(t, e, ValidationErrorDecodeOpenIdConfigurationFailure, http.StatusUnauthorized, decodeError) 90 | 91 | httpGetter.AssertExpectations(t) 92 | configDecoder.AssertExpectations(t) 93 | } 94 | 95 | func TestConfigurationProvider_Get_WhenDecodeResponseSucceeds(t *testing.T) { 96 | httpGetter := &mockHTTPGetter{} 97 | configDecoder := &mockConfigurationDecoder{} 98 | 99 | configurationProvider := httpConfigurationProvider{httpGetter, configDecoder} 100 | config := configuration{"testissuer", "https://testissuer/jwk"} 101 | respBody := "openid configuration" 102 | resp := &http.Response{Body: testBody{bytes.NewBufferString(respBody)}} 103 | httpGetter.On("get", (*http.Request)(nil), mock.Anything).Return(resp, nil) 104 | configDecoder.On("decode", mock.MatchedBy(ioReaderMatcher(t, respBody))).Return(config, nil) 105 | 106 | rc, e := configurationProvider.get(nil, mock.Anything) 107 | 108 | if e != nil { 109 | t.Error("An error was returned but not expected", e) 110 | } 111 | 112 | if rc.Issuer != config.Issuer { 113 | t.Error("Expected issuer", config.Issuer, "but was", rc.Issuer) 114 | } 115 | 116 | if rc.JwksURI != config.JwksURI { 117 | t.Error("Expected jwks uri", config.JwksURI, "but was", rc.JwksURI) 118 | } 119 | 120 | httpGetter.AssertExpectations(t) 121 | } 122 | 123 | func expectValidationError(t *testing.T, e error, vec ValidationErrorCode, status int, inner error) { 124 | if e == nil { 125 | t.Error("An error was expected but not returned") 126 | } 127 | 128 | if ve, ok := e.(*ValidationError); ok { 129 | if ve.Code != vec { 130 | t.Error("Expected error code", vec, "but was", ve.Code) 131 | } 132 | if ve.HTTPStatus != status { 133 | t.Error("Expected HTTP status", status, "but was", ve.HTTPStatus) 134 | } 135 | if inner != nil && ve.Err.Error() != inner.Error() { 136 | t.Error("Expected inner error", inner.Error(), ",but was", ve.Err.Error()) 137 | } 138 | } else { 139 | t.Errorf("Expected error type '*ValidationError' but was %T", e) 140 | } 141 | } 142 | 143 | func ioReaderMatcher(t *testing.T, expectedContent string) func(r io.Reader) bool { 144 | return func(r io.Reader) bool { 145 | b, e := ioutil.ReadAll(r) 146 | 147 | assert.Nil(t, e, "error reading the content provided to decode") 148 | res := string(b) == expectedContent 149 | return res 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /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 | func HTTPGetter(hg HTTPGetFunc) func(*Configuration) error 23 | 24 | // extension points: 25 | 26 | type ErrorHandlerFunc func(error, http.ResponseWriter, *http.Request) bool 27 | type GetProvidersFunc func() ([]Provider, error) 28 | type HTTPGetFunc func(r *http.Request, url string) (*http.Response, error) 29 | 30 | The Example below demonstrates these elements working together. 31 | 32 | Token Parsing 33 | 34 | Both Authenticate and AuthenticateUser middlewares expect the incoming requests to have an HTTP 35 | Authorization header with the content 'Bearer [idToken]' where [idToken] is a valid ID Token issued by 36 | an OP. For instance: 37 | 38 | Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6... 39 | 40 | By default, requests that do not contain an Authorization header with this content will not be forwarded 41 | to the next HTTP handler in the pipeline, instead they will fail back to the client with HTTP status 42 | 400/Bad Request. 43 | 44 | Token Validation 45 | 46 | Once parsed the ID Token will be validated: 47 | 48 | 1) Is the token a valid jwt? 49 | 2) Is the token issued by a known OP? 50 | 3) Is the token issued for a known client? 51 | 4) Is the token valid at the time ('not use before' and 'expire at' claims)? 52 | 5) Is the token signed accordingly? 53 | 54 | The signature validation is done with the public keys retrieved from the jwks_uri published by the OP in 55 | its OIDC metadata (https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). 56 | 57 | The token's issuer and audiences will be verified using a collection of the type Provider. This 58 | collection is retrieved by calling the implementation of the function GetProvidersFunc registered with 59 | the Configuration. 60 | If the token issuer matches the Issuer of any of the providers and the token audience matches at least 61 | one of the ClientIDs of the respective provider then the token is considered valid. 62 | 63 | func myGetProviders() ([]openid.Provider, error) { 64 | p, err := openid.NewProvider("https://accounts.google.com", 65 | []string{"407408718192.apps.googleusercontent.com"}) 66 | // .... 67 | return []openid.Provider{p}, nil 68 | } 69 | 70 | c, _ := openid.NewConfiguration(openid.ProvidersGetter(myGetProviders)) 71 | 72 | In code above only tokens with Issuer claim ('iss') https://accounts.google.com and Audiences claim 73 | ('aud') containing "407408718192.apps.googleusercontent.com" can be valid. 74 | 75 | By default, when the token validation fails for any reason the requests will not be forwarded to the next 76 | handler in the pipeline, instead they will fail back to the client with HTTP status 401/Unauthorized. 77 | 78 | Error Handling 79 | 80 | The default behavior of the Authenticate and AuthenticateUser middlewares upon error conditions is: 81 | the execution pipeline is stopped (the next handler will not be executed), the response will contain 82 | status 400 when a token is not found and 401 when it is invalid, and the response will also contain the 83 | error message. 84 | This behavior can be changed by implementing a function of type ErrorHandlerFunc and registering it 85 | using ErrorHandler with the Configuration. 86 | 87 | type ErrorHandlerFunc func(error, http.ResponseWriter, *http.Request) bool 88 | func ErrorHandler(eh ErrorHandlerFunc) func(*Configuration) error 89 | 90 | For instance: 91 | 92 | func myErrorHandler(e error, w http.ResponseWriter, r *http.Request) bool { 93 | fmt.Fprintf(w, e.Error()) 94 | return false 95 | } 96 | 97 | c, _ := openid.NewConfiguration(openid.ProvidersGetter(myGetProviders), 98 | openid.ErrorHandler(myErrorHandler)) 99 | 100 | In the code above myErrorHandler adds the error message to the response and let the execution 101 | continue to the next handler in the pipeline (returning false) for all error types. 102 | You can use this extension point to fine tune what happens when a specific error is returned by your 103 | implementation of the GetProvidersFunc or even for the error types and codes exported by this 104 | package: 105 | 106 | type ValidationError struct 107 | type ValidationErrorCode uint32 108 | type SetupError struct 109 | type SetupErrorCode uint32 110 | 111 | Authenticate vs AuthenticateUser 112 | 113 | Both middlewares Authenticate and AuthenticateUser behave exactly the same way when it comes to 114 | parsing and validating the ID Token. The only difference is that AuthenticateUser will forward the 115 | information about the user's identity from the ID Token to the next handler in the pipeline. 116 | If your service does not need to know the identity of the authenticated user then Authenticate will 117 | suffice, otherwise your choice is AuthenticateUser. 118 | In order to receive the User information from the AuthenticateUser the next handler in the pipeline 119 | must implement the interface UserHandler with the following function: 120 | 121 | ServeHTTPWithUser(*User, http.ResponseWriter, *http.Request) 122 | 123 | You can also make use of the function adapter UserHandlerFunc as shown in the example below: 124 | 125 | func myHandlerWithUser(u *openid.User, w http.ResponseWriter, r *http.Request) { 126 | fmt.Fprintf(w, "Authenticated! The user is %+v.", u) 127 | } 128 | 129 | http.Handle("/user", openid.AuthenticateUser(c, openid.UserHandlerFunc(myHandlerWithUser))) 130 | */ 131 | package openid 132 | -------------------------------------------------------------------------------- /openid/errors.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/dgrijalva/jwt-go" 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{ 97 | Code: ValidationErrorJwtValidationFailure, 98 | Message: "Jwt token validation failed.", 99 | HTTPStatus: http.StatusUnauthorized, 100 | } 101 | } 102 | 103 | if (jwtError.Errors & jwt.ValidationErrorMalformed) != 0 { 104 | return &ValidationError{ 105 | Code: ValidationErrorJwtValidationFailure, 106 | Message: "Jwt token validation failed.", 107 | HTTPStatus: http.StatusBadRequest, 108 | } 109 | } 110 | 111 | if (jwtError.Errors & jwt.ValidationErrorUnverifiable) != 0 { 112 | // TODO: improve this once https://github.com/dgrijalva/jwt-go/issues/108 is resolved. 113 | // Currently jwt.Parse does not surface errors returned by the KeyFunc. 114 | return &ValidationError{ 115 | Code: ValidationErrorJwtValidationFailure, 116 | Message: jwtError.Error(), 117 | HTTPStatus: http.StatusUnauthorized, 118 | } 119 | } 120 | } 121 | return &ValidationError{ 122 | Code: ValidationErrorJwtValidationUnknownFailure, 123 | Message: "Jwt token validation failed with unknown error.", 124 | HTTPStatus: http.StatusInternalServerError, 125 | } 126 | } 127 | 128 | func validationErrorToHTTPStatus(e error, rw http.ResponseWriter, req *http.Request) (halt bool) { 129 | if verr, ok := e.(*ValidationError); ok { 130 | http.Error(rw, verr.Message, verr.HTTPStatus) 131 | } else { 132 | rw.WriteHeader(http.StatusInternalServerError) 133 | fmt.Fprintf(rw, e.Error()) 134 | } 135 | 136 | return true 137 | } 138 | -------------------------------------------------------------------------------- /openid/example_test.go: -------------------------------------------------------------------------------- 1 | package openid_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/emanoelxavier/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/idtokenvalidator.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "crypto/rsa" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/dgrijalva/jwt-go" 9 | ) 10 | 11 | const issuerClaimName = "iss" 12 | const audiencesClaimName = "aud" 13 | const subjectClaimName = "sub" 14 | const keyIDJwtHeaderName = "kid" 15 | 16 | type jwtTokenValidator interface { 17 | validate(r *http.Request, t string) (jt *jwt.Token, err error) 18 | } 19 | 20 | type jwtParser interface { 21 | parse(string, jwt.Keyfunc) (*jwt.Token, error) 22 | } 23 | 24 | type jwtParserFunc func(string, jwt.Keyfunc) (*jwt.Token, error) 25 | 26 | func (p jwtParserFunc) parse(token string, keyFunc jwt.Keyfunc) (*jwt.Token, error) { 27 | return p(token, keyFunc) 28 | } 29 | 30 | type pemToRSAPublicKeyParser interface { 31 | parse(key []byte) (*rsa.PublicKey, error) 32 | } 33 | 34 | type defaultPemToRSAPublicKeyParser struct { 35 | } 36 | 37 | func (p *defaultPemToRSAPublicKeyParser) parse(key []byte) (*rsa.PublicKey, error) { 38 | return jwt.ParseRSAPublicKeyFromPEM(key) 39 | } 40 | 41 | // type pemToRSAPublicKeyParserFunc func(key []byte) (*rsa.PublicKey, error) 42 | 43 | type idTokenValidator struct { 44 | provGetter providersGetter 45 | jwtParser jwtParser 46 | keyGetter signingKeyGetter 47 | rsaParser pemToRSAPublicKeyParser 48 | } 49 | 50 | func newIDTokenValidator(pg GetProvidersFunc, jp jwtParser, kg signingKeyGetter, kp pemToRSAPublicKeyParser) *idTokenValidator { 51 | return &idTokenValidator{pg, jp, kg, kp} 52 | } 53 | 54 | func (tv *idTokenValidator) validate(r *http.Request, t string) (*jwt.Token, error) { 55 | jt, err := tv.jwtParser.parse(t, func(tok *jwt.Token) (interface{}, error) { 56 | return tv.getSigningKey(r, tok) 57 | }) 58 | if err != nil { 59 | 60 | if verr, ok := err.(*jwt.ValidationError); ok { 61 | // If the signing key did not match it may be because the in memory key is outdated. 62 | // Renew the cached signing key. 63 | if (verr.Errors & jwt.ValidationErrorSignatureInvalid) != 0 { 64 | jt, err = tv.jwtParser.parse(t, func(tok *jwt.Token) (interface{}, error) { 65 | return tv.renewAndGetSigningKey(r, tok) 66 | }) 67 | } 68 | } 69 | } 70 | 71 | if err != nil { 72 | return nil, jwtErrorToOpenIDError(err) 73 | } 74 | 75 | return jt, nil 76 | } 77 | 78 | func (tv *idTokenValidator) renewAndGetSigningKey(r *http.Request, jt *jwt.Token) (interface{}, error) { 79 | // Issuer is already validated when 'getSigningKey was called. 80 | iss := jt.Claims.(jwt.MapClaims)[issuerClaimName].(string) 81 | 82 | err := tv.keyGetter.flushCachedSigningKeys(iss) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | kid := getTokenKid(jt) 88 | 89 | var key []byte 90 | if key, err = tv.keyGetter.getSigningKey(r, iss, kid); err == nil { 91 | return tv.rsaParser.parse(key) 92 | } 93 | 94 | return nil, err 95 | } 96 | 97 | func (tv *idTokenValidator) getSigningKey(r *http.Request, jt *jwt.Token) (interface{}, error) { 98 | provs, err := tv.provGetter.get() 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | if err := providers(provs).validate(); err != nil { 104 | return nil, err 105 | } 106 | 107 | p, err := validateIssuer(jt, provs) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | _, err = validateAudiences(jt, p) 113 | if err != nil { 114 | return nil, err 115 | } 116 | _, err = validateSubject(jt) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | kid := getTokenKid(jt) 122 | 123 | var key []byte 124 | if key, err = tv.keyGetter.getSigningKey(r, p.Issuer, kid); err == nil { 125 | return tv.rsaParser.parse(key) 126 | } 127 | 128 | return nil, err 129 | } 130 | 131 | func getTokenKid(jt *jwt.Token) string { 132 | kid, _ := jt.Header[keyIDJwtHeaderName].(string) 133 | return kid 134 | } 135 | 136 | func validateIssuer(jt *jwt.Token, ps []Provider) (*Provider, error) { 137 | issuerClaim := getIssuer(jt) 138 | var ti string 139 | 140 | if iss, ok := issuerClaim.(string); ok { 141 | ti = iss 142 | } else { 143 | return nil, &ValidationError{ 144 | Code: ValidationErrorInvalidIssuerType, 145 | Message: fmt.Sprintf("Invalid Issuer type: %T", issuerClaim), 146 | HTTPStatus: http.StatusUnauthorized, 147 | } 148 | } 149 | 150 | if ti == "" { 151 | return nil, &ValidationError{ 152 | Code: ValidationErrorInvalidIssuer, 153 | Message: "The token 'iss' claim was not found or was empty.", 154 | HTTPStatus: http.StatusUnauthorized, 155 | } 156 | } 157 | 158 | // Workaround for tokens issued by google 159 | gi := ti 160 | if gi == "accounts.google.com" { 161 | gi = "https://" + gi 162 | } 163 | 164 | for _, p := range ps { 165 | if ti == p.Issuer || gi == p.Issuer { 166 | return &p, nil 167 | } 168 | } 169 | 170 | return nil, &ValidationError{ 171 | Code: ValidationErrorIssuerNotFound, 172 | Message: fmt.Sprintf("No provider was registered with issuer: %v", ti), 173 | HTTPStatus: http.StatusUnauthorized, 174 | } 175 | } 176 | 177 | func validateSubject(jt *jwt.Token) (string, error) { 178 | subjectClaim := getSubject(jt) 179 | 180 | var ts string 181 | if sub, ok := subjectClaim.(string); ok { 182 | ts = sub 183 | } else { 184 | return ts, &ValidationError{ 185 | Code: ValidationErrorInvalidSubjectType, 186 | Message: fmt.Sprintf("Invalid subject type: %T", subjectClaim), 187 | HTTPStatus: http.StatusUnauthorized, 188 | } 189 | } 190 | 191 | if ts == "" { 192 | return ts, &ValidationError{ 193 | Code: ValidationErrorInvalidSubject, 194 | Message: "The token 'sub' claim was not found or was empty.", 195 | HTTPStatus: http.StatusUnauthorized, 196 | } 197 | } 198 | 199 | return ts, nil 200 | } 201 | 202 | func validateAudiences(jt *jwt.Token, p *Provider) (string, error) { 203 | audiencesClaim, err := getAudiences(jt) 204 | 205 | if err != nil { 206 | return "", err 207 | } 208 | 209 | for _, aud := range p.ClientIDs { 210 | for _, audienceClaim := range audiencesClaim { 211 | ta, ok := audienceClaim.(string) 212 | if !ok { 213 | fmt.Printf("aud type %T \n", audienceClaim) 214 | return "", &ValidationError{ 215 | Code: ValidationErrorInvalidAudienceType, 216 | Message: fmt.Sprintf("Invalid Audiences type: %T", audiencesClaim), 217 | HTTPStatus: http.StatusUnauthorized, 218 | } 219 | } 220 | 221 | if ta == "" { 222 | return "", &ValidationError{ 223 | Code: ValidationErrorInvalidAudience, 224 | Message: "The token 'aud' claim was not found or was empty.", 225 | HTTPStatus: http.StatusUnauthorized, 226 | } 227 | } 228 | 229 | if ta == aud { 230 | return ta, nil 231 | } 232 | } 233 | } 234 | 235 | return "", &ValidationError{ 236 | Code: ValidationErrorAudienceNotFound, 237 | Message: fmt.Sprintf("The provider %v does not have a client id matching any of the token audiences %+v", p.Issuer, audiencesClaim), 238 | HTTPStatus: http.StatusUnauthorized, 239 | } 240 | } 241 | 242 | func getAudiences(t *jwt.Token) ([]interface{}, error) { 243 | audiencesClaim := t.Claims.(jwt.MapClaims)[audiencesClaimName] 244 | if aud, ok := audiencesClaim.(string); ok { 245 | return []interface{}{aud}, nil 246 | } else if _, ok := audiencesClaim.([]interface{}); ok { 247 | return audiencesClaim.([]interface{}), nil 248 | } 249 | 250 | return nil, &ValidationError{ 251 | Code: ValidationErrorInvalidAudienceType, 252 | Message: fmt.Sprintf("Invalid Audiences type: %T", audiencesClaim), 253 | HTTPStatus: http.StatusUnauthorized, 254 | } 255 | 256 | } 257 | 258 | func getIssuer(t *jwt.Token) interface{} { 259 | return t.Claims.(jwt.MapClaims)[issuerClaimName] 260 | } 261 | 262 | func getSubject(t *jwt.Token) interface{} { 263 | return t.Claims.(jwt.MapClaims)[subjectClaimName] 264 | } 265 | -------------------------------------------------------------------------------- /openid/idtokenvalidator_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "crypto/rsa" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/dgrijalva/jwt-go" 11 | "github.com/stretchr/testify/mock" 12 | ) 13 | 14 | func Test_getSigningKey_WhenGetProvidersReturnsError(t *testing.T) { 15 | pm, _, _, _, tv := createIDTokenValidator(t) 16 | 17 | ee := errors.New("Error getting providers") 18 | pm.On("get").Return(nil, ee) 19 | 20 | sk, err := tv.getSigningKey(nil, nil) 21 | 22 | if sk != nil { 23 | t.Error("The returned signing key should be nil.") 24 | } 25 | 26 | if err == nil { 27 | t.Fatal("An error was expected but not returned.") 28 | } 29 | 30 | if err.Error() != ee.Error() { 31 | t.Error("Expected error", ee, ", but got", err) 32 | } 33 | 34 | pm.AssertExpectations(t) 35 | } 36 | 37 | func Test_getSigningKey_WhenGetProvidersReturnsEmptyCollection(t *testing.T) { 38 | pm, _, _, _, tv := createIDTokenValidator(t) 39 | 40 | pm.On("get").Return(nil, nil).Once() 41 | 42 | pm.On("get").Return([]Provider{}, nil).Once() 43 | 44 | _, err := tv.getSigningKey(nil, nil) 45 | expectSetupError(t, err, SetupErrorEmptyProviderCollection) 46 | 47 | _, err = tv.getSigningKey(nil, nil) 48 | expectSetupError(t, err, SetupErrorEmptyProviderCollection) 49 | 50 | pm.AssertExpectations(t) 51 | } 52 | 53 | func Test_getSigningKey_UsingTokenWithInvalidIssuerType(t *testing.T) { 54 | pm, _, _, _, tv := createIDTokenValidator(t) 55 | pm.On("get").Return([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil) 56 | 57 | jt := jwt.New(jwt.SigningMethodRS256) 58 | jt.Claims.(jwt.MapClaims)["iss"] = 0 // The expected issuer type is string, not int. 59 | sk, err := tv.getSigningKey(nil, jt) 60 | 61 | if sk != nil { 62 | t.Error("The returned signing key should be nil.") 63 | } 64 | 65 | expectValidationError(t, err, ValidationErrorInvalidIssuerType, http.StatusUnauthorized, nil) 66 | pm.AssertExpectations(t) 67 | } 68 | 69 | func Test_getSigningKey_UsingTokenWithEmptyIssuer(t *testing.T) { 70 | pm, _, _, _, tv := createIDTokenValidator(t) 71 | 72 | pm.On("get").Return([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil).Once() 73 | pm.On("get").Return([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil).Once() 74 | 75 | jt := jwt.New(jwt.SigningMethodRS256) 76 | 77 | // The token has no 'iss' claim 78 | sk, err := tv.getSigningKey(nil, jt) 79 | 80 | if sk != nil { 81 | t.Error("The returned signing key should be nil.") 82 | } 83 | 84 | expectValidationError(t, err, ValidationErrorInvalidIssuerType, http.StatusUnauthorized, nil) 85 | 86 | // The token has '' as 'iss' claim 87 | jt.Claims.(jwt.MapClaims)["iss"] = "" 88 | sk, err = tv.getSigningKey(nil, jt) 89 | 90 | if sk != nil { 91 | t.Error("The returned signing key should be nil.") 92 | } 93 | 94 | expectValidationError(t, err, ValidationErrorInvalidIssuer, http.StatusUnauthorized, nil) 95 | 96 | pm.AssertExpectations(t) 97 | } 98 | 99 | func Test_getSigningKey_UsingTokenWithUnknownIssuer(t *testing.T) { 100 | pm, _, _, _, tv := createIDTokenValidator(t) 101 | 102 | pm.On("get").Return([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil) 103 | 104 | jt := jwt.New(jwt.SigningMethodRS256) 105 | jt.Claims.(jwt.MapClaims)["iss"] = "http://unknown" 106 | 107 | // The token has no 'iss' claim 108 | sk, err := tv.getSigningKey(nil, jt) 109 | 110 | if sk != nil { 111 | t.Error("The returned signing key should be nil.") 112 | } 113 | 114 | expectValidationError(t, err, ValidationErrorIssuerNotFound, http.StatusUnauthorized, nil) 115 | pm.AssertExpectations(t) 116 | } 117 | 118 | func Test_getSigningKey_UsingTokenWithInvalidAudienceType(t *testing.T) { 119 | pm, _, _, _, tv := createIDTokenValidator(t) 120 | 121 | pm.On("get").Return([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil) 122 | 123 | jt := jwt.New(jwt.SigningMethodRS256) 124 | jt.Claims.(jwt.MapClaims)["iss"] = "https://issuer" 125 | jt.Claims.(jwt.MapClaims)["aud"] = 0 // Expected 'aud' type is string 126 | 127 | sk, err := tv.getSigningKey(nil, jt) 128 | 129 | if sk != nil { 130 | t.Error("The returned signing key should be nil.") 131 | } 132 | 133 | expectValidationError(t, err, ValidationErrorInvalidAudienceType, http.StatusUnauthorized, nil) 134 | pm.AssertExpectations(t) 135 | } 136 | 137 | func Test_getSigningKey_UsingTokenWithInvalidAudience(t *testing.T) { 138 | pm, _, _, _, tv := createIDTokenValidator(t) 139 | 140 | pm.On("get").Return([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil).Once() 141 | pm.On("get").Return([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil).Once() 142 | 143 | jt := jwt.New(jwt.SigningMethodRS256) 144 | jt.Claims.(jwt.MapClaims)["iss"] = "https://issuer" 145 | 146 | // No audience claim 147 | sk, err := tv.getSigningKey(nil, jt) 148 | 149 | if sk != nil { 150 | t.Error("The returned signing key should be nil.") 151 | } 152 | 153 | expectValidationError(t, err, ValidationErrorInvalidAudienceType, http.StatusUnauthorized, nil) 154 | 155 | // Empty audience claim. 156 | jt.Claims.(jwt.MapClaims)["aud"] = "" 157 | sk, err = tv.getSigningKey(nil, jt) 158 | 159 | if sk != nil { 160 | t.Error("The returned signing key should be nil.") 161 | } 162 | 163 | expectValidationError(t, err, ValidationErrorInvalidAudience, http.StatusUnauthorized, nil) 164 | pm.AssertExpectations(t) 165 | } 166 | 167 | func Test_getSigningKey_UsingTokenWithUnknownAudience(t *testing.T) { 168 | pm, _, _, _, tv := createIDTokenValidator(t) 169 | 170 | pm.On("get").Return([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client1", "client2"}}}, nil) 171 | 172 | jt := jwt.New(jwt.SigningMethodRS256) 173 | jt.Claims.(jwt.MapClaims)["iss"] = "https://issuer" 174 | jt.Claims.(jwt.MapClaims)["aud"] = "client3" // unknown audience 175 | 176 | sk, err := tv.getSigningKey(nil, jt) 177 | 178 | if sk != nil { 179 | t.Error("The returned signing key should be nil.") 180 | } 181 | 182 | expectValidationError(t, err, ValidationErrorAudienceNotFound, http.StatusUnauthorized, nil) 183 | pm.AssertExpectations(t) 184 | } 185 | 186 | func Test_getSigningKey_UsingTokenWithUnknownMultipleAudiences(t *testing.T) { 187 | pm, _, _, _, tv := createIDTokenValidator(t) 188 | 189 | pm.On("get").Return([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client1", "client2"}}}, nil) 190 | 191 | jt := jwt.New(jwt.SigningMethodRS256) 192 | jt.Claims.(jwt.MapClaims)["iss"] = "https://issuer" 193 | jt.Claims.(jwt.MapClaims)["aud"] = []interface{}{"client3", "client4"} // unknown audiences 194 | 195 | sk, err := tv.getSigningKey(nil, jt) 196 | 197 | if sk != nil { 198 | t.Error("The returned signing key should be nil.") 199 | } 200 | 201 | expectValidationError(t, err, ValidationErrorAudienceNotFound, http.StatusUnauthorized, nil) 202 | pm.AssertExpectations(t) 203 | } 204 | 205 | func Test_getSigningKey_UsingTokenWithInvalidSubjectType(t *testing.T) { 206 | pm, _, _, _, tv := createIDTokenValidator(t) 207 | 208 | pm.On("get").Return([]Provider{{Issuer: "https://issuer", ClientIDs: []string{"client"}}}, nil) 209 | 210 | jt := jwt.New(jwt.SigningMethodRS256) 211 | jt.Claims.(jwt.MapClaims)["iss"] = "https://issuer" 212 | jt.Claims.(jwt.MapClaims)["aud"] = "client" 213 | jt.Claims.(jwt.MapClaims)["sub"] = 0 // The expected 'sub' claim type is string 214 | sk, err := tv.getSigningKey(nil, jt) 215 | 216 | if sk != nil { 217 | t.Error("The returned signing key should be nil.") 218 | } 219 | 220 | expectValidationError(t, err, ValidationErrorInvalidSubjectType, http.StatusUnauthorized, nil) 221 | pm.AssertExpectations(t) 222 | } 223 | 224 | func Test_getSigningKey_UsingValidToken_WhenSigningKeyGetterReturnsError(t *testing.T) { 225 | pm, _, sm, _, tv := createIDTokenValidator(t) 226 | 227 | req := httptest.NewRequest(http.MethodGet, "/", nil) 228 | iss := "https://issuer" 229 | keyID := "kid" 230 | ee := &ValidationError{Code: ValidationErrorIssuerNotFound, HTTPStatus: http.StatusUnauthorized} 231 | 232 | sm.On("getSigningKey", req, iss, keyID).Return(nil, ee) 233 | pm.On("get").Return([]Provider{{Issuer: iss, ClientIDs: []string{"client"}}}, nil) 234 | 235 | jt := jwt.New(jwt.SigningMethodRS256) 236 | jt.Claims.(jwt.MapClaims)["iss"] = iss 237 | jt.Claims.(jwt.MapClaims)["aud"] = "client" 238 | jt.Claims.(jwt.MapClaims)["sub"] = "subject1" 239 | jt.Header["kid"] = keyID 240 | 241 | _, err := tv.getSigningKey(req, jt) 242 | 243 | expectValidationError(t, err, ee.Code, ee.HTTPStatus, nil) 244 | pm.AssertExpectations(t) 245 | sm.AssertExpectations(t) 246 | } 247 | 248 | func Test_getSigningKey_UsingValidToken_WhenSigningKeyGetterSucceeds(t *testing.T) { 249 | pm, _, sm, kp, tv := createIDTokenValidator(t) 250 | 251 | req := httptest.NewRequest(http.MethodGet, "/", nil) 252 | iss := "https://issuer" 253 | keyID := "kid" 254 | esk := "signingKey" 255 | pk := &rsa.PublicKey{N: nil, E: 345} 256 | 257 | sm.On("getSigningKey", req, iss, keyID).Return([]byte(esk), nil) 258 | pm.On("get").Return([]Provider{{Issuer: iss, ClientIDs: []string{"client"}}}, nil) 259 | kp.On("parse", []byte(esk)).Return(pk, nil) 260 | 261 | jt := jwt.New(jwt.SigningMethodRS256) 262 | jt.Claims.(jwt.MapClaims)["iss"] = iss 263 | jt.Claims.(jwt.MapClaims)["aud"] = "client" 264 | jt.Claims.(jwt.MapClaims)["sub"] = "subject1" 265 | jt.Header["kid"] = keyID 266 | 267 | rsk, err := tv.getSigningKey(req, jt) 268 | 269 | if err != nil { 270 | t.Error("An error was returned but not expected.", err) 271 | } 272 | 273 | expectSigningKey(t, rsk, jt, pk) 274 | 275 | pm.AssertExpectations(t) 276 | sm.AssertExpectations(t) 277 | kp.AssertExpectations(t) 278 | } 279 | 280 | func Test_getSigningKey_UsingValidToken_WithoutKeyIdentifier_WhenSigningKeyGetterSucceeds(t *testing.T) { 281 | pm, _, sm, kp, tv := createIDTokenValidator(t) 282 | 283 | iss := "https://issuer" 284 | keyID := "" 285 | esk := "signingKey" 286 | pk := &rsa.PublicKey{N: nil, E: 345} 287 | sm.On("getSigningKey", (*http.Request)(nil), iss, keyID).Return([]byte(esk), nil) 288 | pm.On("get").Return([]Provider{{Issuer: iss, ClientIDs: []string{"client"}}}, nil) 289 | kp.On("parse", []byte(esk)).Return(pk, nil) 290 | 291 | jt := jwt.New(jwt.SigningMethodRS256) 292 | jt.Claims.(jwt.MapClaims)["iss"] = iss 293 | jt.Claims.(jwt.MapClaims)["aud"] = "client" 294 | jt.Claims.(jwt.MapClaims)["sub"] = "subject1" 295 | 296 | rsk, err := tv.getSigningKey(nil, jt) 297 | 298 | if err != nil { 299 | t.Error("An error was returned but not expected.", err) 300 | } 301 | 302 | expectSigningKey(t, rsk, jt, pk) 303 | 304 | pm.AssertExpectations(t) 305 | sm.AssertExpectations(t) 306 | sm.AssertExpectations(t) 307 | kp.AssertExpectations(t) 308 | } 309 | 310 | func Test_getSigningKey_UsingValidTokenWithMultipleAudiences(t *testing.T) { 311 | pm, _, sm, kp, tv := createIDTokenValidator(t) 312 | 313 | iss := "https://issuer" 314 | keyID := "kid" 315 | esk := "signingKey" 316 | pk := &rsa.PublicKey{N: nil, E: 345} 317 | 318 | sm.On("getSigningKey", (*http.Request)(nil), iss, keyID).Return([]byte(esk), nil) 319 | pm.On("get").Return([]Provider{{Issuer: iss, ClientIDs: []string{"client"}}}, nil) 320 | kp.On("parse", []byte(esk)).Return(pk, nil) 321 | 322 | jt := jwt.New(jwt.SigningMethodRS256) 323 | jt.Claims.(jwt.MapClaims)["iss"] = iss 324 | jt.Claims.(jwt.MapClaims)["aud"] = []interface{}{"unknown", "client"} 325 | jt.Claims.(jwt.MapClaims)["sub"] = "subject1" 326 | jt.Header["kid"] = keyID 327 | 328 | rsk, err := tv.getSigningKey(nil, jt) 329 | 330 | if err != nil { 331 | t.Error("An error was returned but not expected.", err) 332 | } 333 | 334 | expectSigningKey(t, rsk, jt, pk) 335 | 336 | pm.AssertExpectations(t) 337 | sm.AssertExpectations(t) 338 | kp.AssertExpectations(t) 339 | } 340 | 341 | func Test_renewAndGetSigningKey_UsingValidToken_WhenFlushCachedSigningKeysReturnsError(t *testing.T) { 342 | _, _, sm, _, tv := createIDTokenValidator(t) 343 | 344 | ee := &ValidationError{Code: ValidationErrorIssuerNotFound, HTTPStatus: http.StatusUnauthorized} 345 | sm.On("flushCachedSigningKeys", mock.Anything).Return(ee) 346 | 347 | jt := jwt.New(jwt.SigningMethodRS256) 348 | jt.Claims.(jwt.MapClaims)["iss"] = "" 349 | 350 | _, err := tv.renewAndGetSigningKey(nil, jt) 351 | 352 | expectValidationError(t, err, ee.Code, ee.HTTPStatus, nil) 353 | 354 | sm.AssertExpectations(t) 355 | } 356 | 357 | func Test_renewAndGetSigningKey_UsingValidToken_WhenGetSigningKeyReturnsError(t *testing.T) { 358 | _, _, sm, _, tv := createIDTokenValidator(t) 359 | 360 | ee := &ValidationError{Code: ValidationErrorIssuerNotFound, HTTPStatus: http.StatusUnauthorized} 361 | sm.On("getSigningKey", (*http.Request)(nil), mock.Anything, mock.Anything).Return(nil, ee) 362 | sm.On("flushCachedSigningKeys", mock.Anything).Return(nil) 363 | 364 | jt := jwt.New(jwt.SigningMethodRS256) 365 | jt.Claims.(jwt.MapClaims)["iss"] = "" 366 | jt.Header["kid"] = "" 367 | 368 | _, err := tv.renewAndGetSigningKey(nil, jt) 369 | 370 | expectValidationError(t, err, ee.Code, ee.HTTPStatus, nil) 371 | 372 | sm.AssertExpectations(t) 373 | } 374 | 375 | func Test_renewAndGetSigningKey_UsingValidToken_WhenGetSigningKeySucceeds(t *testing.T) { 376 | _, _, sm, kp, tv := createIDTokenValidator(t) 377 | esk := "signingKey" 378 | pk := &rsa.PublicKey{N: nil, E: 365} 379 | 380 | sm.On("getSigningKey", (*http.Request)(nil), mock.Anything, mock.Anything).Return([]byte(esk), nil) 381 | sm.On("flushCachedSigningKeys", mock.Anything).Return(nil) 382 | kp.On("parse", []byte(esk)).Return(pk, nil) 383 | 384 | jt := jwt.New(jwt.SigningMethodRS256) 385 | jt.Claims.(jwt.MapClaims)["iss"] = "" 386 | jt.Header["kid"] = "" 387 | 388 | rsk, err := tv.renewAndGetSigningKey(nil, jt) 389 | 390 | if err != nil { 391 | t.Error("An error was returned but not expected.", err) 392 | } 393 | 394 | expectSigningKey(t, rsk, jt, pk) 395 | sm.AssertExpectations(t) 396 | kp.AssertExpectations(t) 397 | } 398 | 399 | func Test_validate_WhenParserReturnsErrorFirstTime(t *testing.T) { 400 | _, jm, _, _, tv := createIDTokenValidator(t) 401 | 402 | je := &jwt.ValidationError{Errors: jwt.ValidationErrorNotValidYet} 403 | ee := &ValidationError{Code: ValidationErrorJwtValidationFailure, HTTPStatus: http.StatusUnauthorized} 404 | 405 | jm.On("parse", mock.Anything, mock.AnythingOfType("jwt.Keyfunc")).Return(nil, je) 406 | 407 | _, err := tv.validate(nil, mock.Anything) 408 | 409 | expectValidationError(t, err, ee.Code, ee.HTTPStatus, ee.Err) 410 | 411 | jm.AssertExpectations(t) 412 | } 413 | 414 | func Test_validate_WhenParserSuceedsFirstTime(t *testing.T) { 415 | _, jm, _, _, tv := createIDTokenValidator(t) 416 | 417 | jt := &jwt.Token{} 418 | 419 | jm.On("parse", mock.Anything, mock.AnythingOfType("jwt.Keyfunc")).Return(jt, nil) 420 | 421 | rjt, err := tv.validate(nil, mock.Anything) 422 | 423 | if err != nil { 424 | t.Error("Unexpected error was returned.", err) 425 | } 426 | 427 | if rjt != jt { 428 | t.Errorf("Expected %+v, but got %+v.", jt, rjt) 429 | } 430 | 431 | jm.AssertExpectations(t) 432 | } 433 | 434 | func Test_validate_WhenParserReturnsErrorSecondTime(t *testing.T) { 435 | _, jm, _, _, tv := createIDTokenValidator(t) 436 | 437 | jfe := &jwt.ValidationError{Errors: jwt.ValidationErrorSignatureInvalid} 438 | je := &jwt.ValidationError{Errors: jwt.ValidationErrorMalformed} 439 | ee := &ValidationError{Code: ValidationErrorJwtValidationFailure, HTTPStatus: http.StatusBadRequest} 440 | 441 | jm.On("parse", mock.Anything, mock.AnythingOfType("jwt.Keyfunc")).Return(nil, jfe).Once() 442 | jm.On("parse", mock.Anything, mock.AnythingOfType("jwt.Keyfunc")).Return(nil, je).Once() 443 | 444 | _, err := tv.validate(nil, mock.Anything) 445 | 446 | expectValidationError(t, err, ee.Code, ee.HTTPStatus, ee.Err) 447 | 448 | jm.AssertExpectations(t) 449 | } 450 | 451 | func Test_validate_WhenParserReturnsSignatureInvalidErrorSecondTime(t *testing.T) { 452 | _, jm, _, _, tv := createIDTokenValidator(t) 453 | 454 | je := &jwt.ValidationError{Errors: jwt.ValidationErrorSignatureInvalid} 455 | ee := &ValidationError{Code: ValidationErrorJwtValidationFailure, HTTPStatus: http.StatusUnauthorized} 456 | 457 | jm.On("parse", mock.Anything, mock.AnythingOfType("jwt.Keyfunc")).Return(nil, je).Once() 458 | jm.On("parse", mock.Anything, mock.AnythingOfType("jwt.Keyfunc")).Return(nil, je).Once() 459 | 460 | _, err := tv.validate(nil, mock.Anything) 461 | 462 | expectValidationError(t, err, ee.Code, ee.HTTPStatus, ee.Err) 463 | 464 | jm.AssertExpectations(t) 465 | } 466 | 467 | func Test_validate_WhenParserSuceedsSecondTime(t *testing.T) { 468 | _, jm, _, _, tv := createIDTokenValidator(t) 469 | 470 | jfe := &jwt.ValidationError{Errors: jwt.ValidationErrorSignatureInvalid} 471 | 472 | jt := &jwt.Token{} 473 | 474 | jm.On("parse", mock.Anything, mock.AnythingOfType("jwt.Keyfunc")).Return(jt, jfe).Once() 475 | jm.On("parse", mock.Anything, mock.AnythingOfType("jwt.Keyfunc")).Return(jt, nil).Once() 476 | 477 | rjt, err := tv.validate(nil, mock.Anything) 478 | if err != nil { 479 | t.Error("Unexpected error was returned.", err) 480 | } 481 | 482 | if rjt != jt { 483 | t.Errorf("Expected %+v, but got %+v.", jt, rjt) 484 | } 485 | 486 | jm.AssertExpectations(t) 487 | } 488 | 489 | func expectSigningKey(t *testing.T, rsk interface{}, jt *jwt.Token, esk *rsa.PublicKey) { 490 | 491 | if rsk == nil { 492 | t.Fatal("The returned signing key was nil.") 493 | } 494 | 495 | if sk, ok := rsk.(*rsa.PublicKey); ok { 496 | if sk.E != esk.E { 497 | t.Error("Expected signing key", esk, "but got", sk) 498 | } 499 | } else { 500 | t.Errorf("Expected signing key type '*rsa.PublicKey', but got %T", rsk) 501 | } 502 | } 503 | 504 | func createIDTokenValidator(t *testing.T) (*mockProvidersGetter, *mockJwtParser, *mockSigningKeyGetter, *mockPemToRSAPublicKeyParser, *idTokenValidator) { 505 | pm := &mockProvidersGetter{} 506 | jm := &mockJwtParser{} 507 | sm := &mockSigningKeyGetter{} 508 | kp := &mockPemToRSAPublicKeyParser{} 509 | return pm, jm, sm, kp, &idTokenValidator{pm, jm, sm, kp} 510 | } 511 | -------------------------------------------------------------------------------- /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/emanoelxavier/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/jwksprovider.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | jose "gopkg.in/square/go-jose.v2" 9 | ) 10 | 11 | type jwksGetter interface { 12 | get(r *http.Request, url string) (jose.JSONWebKeySet, error) 13 | } 14 | 15 | type jwksDecoder interface { 16 | decode(io.Reader) (jose.JSONWebKeySet, error) 17 | } 18 | 19 | type httpJwksProvider struct { 20 | getter httpGetter 21 | decoder jwksDecoder 22 | } 23 | 24 | func newHTTPJwksProvider(gf HTTPGetFunc, d jwksDecoder) *httpJwksProvider { 25 | return &httpJwksProvider{gf, d} 26 | } 27 | 28 | func (httpProv *httpJwksProvider) get(r *http.Request, url string) (jose.JSONWebKeySet, error) { 29 | 30 | var jwks jose.JSONWebKeySet 31 | resp, err := httpProv.getter.get(r, url) 32 | 33 | if err != nil { 34 | return jwks, &ValidationError{ 35 | Code: ValidationErrorGetJwksFailure, 36 | Message: fmt.Sprintf("Failure while contacting the jwk endpoint %v.", url), 37 | Err: err, 38 | HTTPStatus: http.StatusUnauthorized, 39 | } 40 | } 41 | 42 | defer resp.Body.Close() 43 | 44 | if jwks, err = httpProv.decoder.decode(resp.Body); err != nil { 45 | return jwks, &ValidationError{ 46 | Code: ValidationErrorDecodeJwksFailure, 47 | Message: fmt.Sprintf("Failure while decoding the jwk retrieved from the endpoint %v.", url), 48 | Err: err, 49 | HTTPStatus: http.StatusUnauthorized, 50 | } 51 | } 52 | 53 | return jwks, nil 54 | } 55 | 56 | type jsonJwksDecoder struct { 57 | } 58 | 59 | func (d *jsonJwksDecoder) decode(r io.Reader) (jose.JSONWebKeySet, error) { 60 | var jwks jose.JSONWebKeySet 61 | err := jsonDecodeResponse(r, &jwks) 62 | 63 | return jwks, err 64 | } 65 | -------------------------------------------------------------------------------- /openid/jwksprovider_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/mock" 11 | "gopkg.in/square/go-jose.v2" 12 | ) 13 | 14 | func TestJwksProvider_Get_UsesCorrectUrl(t *testing.T) { 15 | httpGetter := &mockHTTPGetter{} 16 | jwksProvider := httpJwksProvider{getter: httpGetter} 17 | req := httptest.NewRequest(http.MethodGet, "/", nil) 18 | 19 | url := "https://jwks" 20 | 21 | httpGetter.On("get", req, url).Return(nil, errors.New("Read configuration error")) 22 | 23 | _, e := jwksProvider.get(req, url) 24 | 25 | if e == nil { 26 | t.Error("An error was expected but not returned") 27 | } 28 | 29 | httpGetter.AssertExpectations(t) 30 | } 31 | 32 | func TestJwksProvider_Get_WhenGetReturnsError(t *testing.T) { 33 | httpGetter := &mockHTTPGetter{} 34 | jwksProvider := httpJwksProvider{getter: httpGetter} 35 | 36 | readError := errors.New("Read jwks error") 37 | httpGetter.On("get", (*http.Request)(nil), mock.Anything).Return(nil, readError) 38 | 39 | _, e := jwksProvider.get(nil, mock.Anything) 40 | 41 | expectValidationError(t, e, ValidationErrorGetJwksFailure, http.StatusUnauthorized, readError) 42 | 43 | httpGetter.AssertExpectations(t) 44 | } 45 | 46 | func TestJwksProvider_Get_WhenGetSucceeds(t *testing.T) { 47 | httpGetter := &mockHTTPGetter{} 48 | jwksDecoder := &mockJwksDecoder{} 49 | jwksProvider := httpJwksProvider{httpGetter, jwksDecoder} 50 | 51 | respBody := "jwk set" 52 | resp := &http.Response{Body: testBody{bytes.NewBufferString(respBody)}} 53 | httpGetter.On("get", (*http.Request)(nil), mock.Anything).Return(resp, nil) 54 | jwksDecoder.On("decode", mock.MatchedBy(ioReaderMatcher(t, respBody))).Return(jose.JSONWebKeySet{}, nil) 55 | 56 | _, e := jwksProvider.get(nil, mock.Anything) 57 | 58 | if e != nil { 59 | t.Error("An error was returned but not expected", e) 60 | } 61 | 62 | httpGetter.AssertExpectations(t) 63 | jwksDecoder.AssertExpectations(t) 64 | } 65 | 66 | func TestJwksProvider_Get_WhenDecodeResponseReturnsError(t *testing.T) { 67 | httpGetter := &mockHTTPGetter{} 68 | jwksDecoder := &mockJwksDecoder{} 69 | 70 | jwksProvider := httpJwksProvider{httpGetter, jwksDecoder} 71 | decodeError := errors.New("Decode jwks error") 72 | respBody := "jwk set." 73 | resp := &http.Response{Body: testBody{bytes.NewBufferString(respBody)}} 74 | httpGetter.On("get", (*http.Request)(nil), mock.Anything).Return(resp, nil) 75 | jwksDecoder.On("decode", mock.Anything).Return(jose.JSONWebKeySet{}, decodeError) 76 | 77 | _, e := jwksProvider.get(nil, mock.Anything) 78 | 79 | expectValidationError(t, e, ValidationErrorDecodeJwksFailure, http.StatusUnauthorized, decodeError) 80 | 81 | httpGetter.AssertExpectations(t) 82 | jwksDecoder.AssertExpectations(t) 83 | } 84 | 85 | func TestJwksProvider_Get_WhenDecodeResponseSucceeds(t *testing.T) { 86 | httpGetter := &mockHTTPGetter{} 87 | jwksDecoder := &mockJwksDecoder{} 88 | 89 | jwksProvider := httpJwksProvider{httpGetter, jwksDecoder} 90 | keys := []jose.JSONWebKey{ 91 | {Key: "key1", Certificates: nil, KeyID: "keyid1", Algorithm: "algo1", Use: "use1"}, 92 | {Key: "key2", Certificates: nil, KeyID: "keyid2", Algorithm: "algo2", Use: "use2"}, 93 | } 94 | jwks := jose.JSONWebKeySet{Keys: keys} 95 | respBody := "jwk set" 96 | resp := &http.Response{Body: testBody{bytes.NewBufferString(respBody)}} 97 | httpGetter.On("get", (*http.Request)(nil), mock.Anything).Return(resp, nil) 98 | jwksDecoder.On("decode", mock.Anything).Return(jwks, nil) 99 | 100 | rj, e := jwksProvider.get(nil, mock.Anything) 101 | 102 | if e != nil { 103 | t.Error("An error was returned but not expected", e) 104 | } 105 | 106 | if len(rj.Keys) != len(jwks.Keys) { 107 | t.Fatal("Expected", len(jwks.Keys), "keys, but got", len(rj.Keys)) 108 | } 109 | 110 | for i, key := range rj.Keys { 111 | ek := jwks.Keys[i] 112 | if key.Algorithm != ek.Algorithm { 113 | t.Errorf("Key algorithm at %v should be %v, but was %v", i, ek.Algorithm, key.Algorithm) 114 | } 115 | if key.KeyID != ek.KeyID { 116 | t.Errorf("Key ID at %v should be %v, but was %v", i, ek.KeyID, key.KeyID) 117 | } 118 | if key.Key != ek.Key { 119 | t.Errorf("Key at %v should be %v, but was %v", i, ek.Key, key.Key) 120 | } 121 | if key.Use != ek.Use { 122 | t.Errorf("Key Use at %v should be %v, but was %v", i, ek.Use, key.Use) 123 | } 124 | } 125 | 126 | httpGetter.AssertExpectations(t) 127 | jwksDecoder.AssertExpectations(t) 128 | } 129 | -------------------------------------------------------------------------------- /openid/middleware.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/dgrijalva/jwt-go" 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 | // 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(defaultHTTPGet, &jsonConfigurationDecoder{}) 26 | jp := newHTTPJwksProvider(defaultHTTPGet, &jsonJwksDecoder{}) 27 | ksp := newSigningKeySetProvider(cp, jp, &pemPublicKeyEncoder{}) 28 | kp := newSigningKeyProvider(ksp) 29 | m.tokenValidator = newIDTokenValidator(nil, jwtParserFunc(jwt.Parse), kp, &defaultPemToRSAPublicKeyParser{}) 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 | // 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 | // ErrorHandler option registers the function responsible for handling 52 | // the errors returned during token validation. When this option is not used then the 53 | // middleware will use the default internal implementation validationErrorToHTTPStatus. 54 | func ErrorHandler(eh ErrorHandlerFunc) func(*Configuration) error { 55 | return func(c *Configuration) error { 56 | c.errorHandler = eh 57 | return nil 58 | } 59 | } 60 | 61 | // HTTPGetFunc is a function that gets a URL based on a contextual request 62 | // and a target URL. The default behavior is the http.Get method, ignoring 63 | // the request parameter. 64 | type HTTPGetFunc func(r *http.Request, url string) (*http.Response, error) 65 | 66 | var defaultHTTPGet = func(r *http.Request, url string) (*http.Response, error) { 67 | return http.Get(url) 68 | } 69 | 70 | // HTTPGetter option registers the function responsible for returning the 71 | // providers containing the valid issuer and client IDs used to validate the ID Token. 72 | func HTTPGetter(hg HTTPGetFunc) func(*Configuration) error { 73 | return func(c *Configuration) error { 74 | sksp := c.tokenValidator.(*idTokenValidator). 75 | keyGetter.(*signingKeyProvider). 76 | keySetGetter.(*signingKeySetProvider) 77 | sksp.configGetter.(*httpConfigurationProvider).getter = hg 78 | sksp.jwksGetter.(*httpJwksProvider).getter = hg 79 | return nil 80 | } 81 | } 82 | 83 | // Authenticate middleware performs the validation of the OIDC ID Token. 84 | // If an error happens, i.e.: expired token, the next handler may or may not executed depending on the 85 | // provided ErrorHandlerFunc option. The default behavior, determined by validationErrorToHTTPStatus, 86 | // stops the execution and returns Unauthorized. 87 | // If the validation is successful then the next handler(h) will be executed. 88 | func Authenticate(conf *Configuration, h http.Handler) http.Handler { 89 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 90 | if _, halt := authenticate(conf, w, r); !halt { 91 | h.ServeHTTP(w, r) 92 | } 93 | }) 94 | } 95 | 96 | // AuthenticateUser middleware performs the validation of the OIDC ID Token and 97 | // forwards the authenticated user's information to the next handler in the pipeline. 98 | // If an error happens, i.e.: expired token, the next handler may or may not executed depending on the 99 | // provided ErrorHandlerFunc option. The default behavior, determined by validationErrorToHTTPStatus, 100 | // stops the execution and returns Unauthorized. 101 | // If the validation is successful then the next handler(h) will be executed and will 102 | // receive the authenticated user information. 103 | func AuthenticateUser(conf *Configuration, h UserHandler) http.Handler { 104 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 105 | if u, halt := authenticateUser(conf, w, r); !halt { 106 | h.ServeHTTPWithUser(u, w, r) 107 | } 108 | }) 109 | } 110 | 111 | func authenticate(c *Configuration, rw http.ResponseWriter, req *http.Request) (t *jwt.Token, halt bool) { 112 | var tg GetIDTokenFunc 113 | if c.idTokenGetter == nil { 114 | tg = getIDTokenAuthorizationHeader 115 | } else { 116 | tg = c.idTokenGetter 117 | } 118 | 119 | var eh ErrorHandlerFunc 120 | if c.errorHandler == nil { 121 | eh = validationErrorToHTTPStatus 122 | } else { 123 | eh = c.errorHandler 124 | } 125 | 126 | ts, err := tg(req) 127 | 128 | if err != nil { 129 | return nil, eh(err, rw, req) 130 | } 131 | 132 | vt, err := c.tokenValidator.validate(req, ts) 133 | 134 | if err != nil { 135 | return nil, eh(err, rw, req) 136 | } 137 | 138 | return vt, false 139 | } 140 | 141 | func authenticateUser(c *Configuration, rw http.ResponseWriter, req *http.Request) (u *User, halt bool) { 142 | var vt *jwt.Token 143 | 144 | var eh ErrorHandlerFunc 145 | if c.errorHandler == nil { 146 | eh = validationErrorToHTTPStatus 147 | } else { 148 | eh = c.errorHandler 149 | } 150 | 151 | if t, halt := authenticate(c, rw, req); !halt { 152 | vt = t 153 | } else { 154 | return nil, halt 155 | } 156 | 157 | u, err := newUser(vt) 158 | 159 | if err != nil { 160 | return nil, eh(err, rw, req) 161 | } 162 | 163 | return u, false 164 | } 165 | -------------------------------------------------------------------------------- /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/dgrijalva/jwt-go" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | const idToken string = "IDTOKEN" 14 | 15 | func Test_authenticateUser_WhenGetIDTokenReturnsError_WhenErrorHandlerContinues(t *testing.T) { 16 | _, c := createConfiguration(t, errorHandlerContinue, getIDTokenReturnsError) 17 | 18 | u, halt := authenticateUser(c, httptest.NewRecorder(), nil) 19 | 20 | if u != nil { 21 | t.Errorf("The returned user should be nil, but was %+v.", u) 22 | } 23 | 24 | if halt { 25 | t.Error("The authentication should have returned 'halt' false.") 26 | } 27 | } 28 | 29 | func Test_authenticateUser_WhenGetIDTokenReturnsError_WhenErrorHandlerHalts(t *testing.T) { 30 | _, c := createConfiguration(t, errorHandlerHalt, getIDTokenReturnsError) 31 | 32 | u, halt := authenticateUser(c, httptest.NewRecorder(), nil) 33 | 34 | if u != nil { 35 | t.Errorf("The returned user should be nil, but was %+v.", u) 36 | } 37 | 38 | if !halt { 39 | t.Error("The authentication should have returned 'halt' true.") 40 | } 41 | } 42 | 43 | func Test_authenticateUser_WhenValidateReturnsError_WhenErrorHandlerHalts(t *testing.T) { 44 | vm, c := createConfiguration(t, errorHandlerHalt, getIDTokenReturnsSuccess) 45 | vm.On("validate", mock.Anything, idToken).Return(nil, errors.New("Error while validating the token")) 46 | 47 | u, halt := authenticateUser(c, httptest.NewRecorder(), nil) 48 | 49 | if u != nil { 50 | t.Errorf("The returned user should be nil, but was %+v.", u) 51 | } 52 | 53 | if !halt { 54 | t.Error("The authentication should have returned 'halt' true.") 55 | } 56 | 57 | vm.AssertExpectations(t) 58 | } 59 | 60 | func Test_authenticateUser_WhenValidateSucceeds(t *testing.T) { 61 | vm, c := createConfiguration(t, errorHandlerHalt, getIDTokenReturnsSuccess) 62 | iss := "https://issuer" 63 | sub := "SUB1" 64 | 65 | jt := jwt.New(jwt.SigningMethodRS256) 66 | jt.Claims.(jwt.MapClaims)["iss"] = iss 67 | jt.Claims.(jwt.MapClaims)["sub"] = sub 68 | 69 | vm.On("validate", mock.Anything, idToken).Return(jt, nil) 70 | 71 | u, halt := authenticateUser(c, httptest.NewRecorder(), nil) 72 | 73 | if halt { 74 | t.Error("A successful authenticateUser call should not have returned halt with value true.") 75 | } 76 | 77 | if u == nil { 78 | t.Fatal("The returned user should not be nil.") 79 | } 80 | 81 | if u.Issuer != iss { 82 | t.Error("Expected user issuer", iss, ", but got", u.Issuer) 83 | } 84 | 85 | if u.ID != sub { 86 | t.Error("Expected user ID", sub, ", but got", u.ID) 87 | } 88 | 89 | if len(u.Claims) != len(jt.Claims.(jwt.MapClaims)) { 90 | t.Error("Expected number of user claims", len(jt.Claims.(jwt.MapClaims)), ", but got", len(u.Claims)) 91 | } 92 | 93 | vm.AssertExpectations(t) 94 | } 95 | 96 | func createConfiguration(t *testing.T, eh ErrorHandlerFunc, gt GetIDTokenFunc) (*mockJwtTokenValidator, *Configuration) { 97 | jm := &mockJwtTokenValidator{} 98 | c, _ := NewConfiguration(ErrorHandler(eh)) 99 | c.tokenValidator = jm 100 | c.idTokenGetter = gt 101 | return jm, c 102 | } 103 | 104 | func getIDTokenReturnsError(r *http.Request) (string, error) { 105 | return "", errors.New("An error happened when returning ID Token") 106 | } 107 | 108 | func getIDTokenReturnsSuccess(r *http.Request) (string, error) { 109 | return idToken, nil 110 | } 111 | 112 | func errorHandlerHalt(e error, w http.ResponseWriter, r *http.Request) bool { 113 | if e != nil { 114 | return true 115 | } 116 | return false 117 | } 118 | 119 | func errorHandlerContinue(e error, w http.ResponseWriter, r *http.Request) bool { 120 | return false 121 | } 122 | -------------------------------------------------------------------------------- /openid/mocks_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0 2 | package openid 3 | 4 | import ( 5 | io "io" 6 | http "net/http" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | jose "gopkg.in/square/go-jose.v2" 10 | 11 | rsa "crypto/rsa" 12 | 13 | jwt "github.com/dgrijalva/jwt-go" 14 | ) 15 | 16 | // mockSigningKeyGetter is an autogenerated mock type for the signingKeyGetter type 17 | type mockSigningKeyGetter struct { 18 | mock.Mock 19 | } 20 | 21 | // flushCachedSigningKeys provides a mock function with given fields: issuer 22 | func (_m *mockSigningKeyGetter) flushCachedSigningKeys(issuer string) error { 23 | ret := _m.Called(issuer) 24 | 25 | var r0 error 26 | if rf, ok := ret.Get(0).(func(string) error); ok { 27 | r0 = rf(issuer) 28 | } else { 29 | r0 = ret.Error(0) 30 | } 31 | 32 | return r0 33 | } 34 | 35 | // getSigningKey provides a mock function with given fields: r, issuer, kid 36 | func (_m *mockSigningKeyGetter) getSigningKey(r *http.Request, issuer string, kid string) ([]byte, error) { 37 | ret := _m.Called(r, issuer, kid) 38 | 39 | var r0 []byte 40 | if rf, ok := ret.Get(0).(func(*http.Request, string, string) []byte); ok { 41 | r0 = rf(r, issuer, kid) 42 | } else { 43 | if ret.Get(0) != nil { 44 | r0 = ret.Get(0).([]byte) 45 | } 46 | } 47 | 48 | var r1 error 49 | if rf, ok := ret.Get(1).(func(*http.Request, string, string) error); ok { 50 | r1 = rf(r, issuer, kid) 51 | } else { 52 | r1 = ret.Error(1) 53 | } 54 | 55 | return r0, r1 56 | } 57 | 58 | // mockJwksGetter is an autogenerated mock type for the jwksGetter type 59 | type mockJwksGetter struct { 60 | mock.Mock 61 | } 62 | 63 | // getJwkSet provides a mock function with given fields: r, url 64 | func (_m *mockJwksGetter) get(r *http.Request, url string) (jose.JSONWebKeySet, error) { 65 | ret := _m.Called(r, url) 66 | 67 | var r0 jose.JSONWebKeySet 68 | if rf, ok := ret.Get(0).(func(*http.Request, string) jose.JSONWebKeySet); ok { 69 | r0 = rf(r, url) 70 | } else { 71 | r0 = ret.Get(0).(jose.JSONWebKeySet) 72 | } 73 | 74 | var r1 error 75 | if rf, ok := ret.Get(1).(func(*http.Request, string) error); ok { 76 | r1 = rf(r, url) 77 | } else { 78 | r1 = ret.Error(1) 79 | } 80 | 81 | return r0, r1 82 | } 83 | 84 | // mockConfigurationGetter is an autogenerated mock type for the configurationGetter type 85 | type mockConfigurationGetter struct { 86 | mock.Mock 87 | } 88 | 89 | // getConfiguration provides a mock function with given fields: r, url 90 | func (_m *mockConfigurationGetter) get(r *http.Request, url string) (configuration, error) { 91 | ret := _m.Called(r, url) 92 | 93 | var r0 configuration 94 | if rf, ok := ret.Get(0).(func(*http.Request, string) configuration); ok { 95 | r0 = rf(r, url) 96 | } else { 97 | r0 = ret.Get(0).(configuration) 98 | } 99 | 100 | var r1 error 101 | if rf, ok := ret.Get(1).(func(*http.Request, string) error); ok { 102 | r1 = rf(r, url) 103 | } else { 104 | r1 = ret.Error(1) 105 | } 106 | 107 | return r0, r1 108 | } 109 | 110 | // mockJwtTokenValidator is an autogenerated mock type for the jwtTokenValidator type 111 | type mockJwtTokenValidator struct { 112 | mock.Mock 113 | } 114 | 115 | // validate provides a mock function with given fields: r, t 116 | func (_m *mockJwtTokenValidator) validate(r *http.Request, t string) (*jwt.Token, error) { 117 | ret := _m.Called(r, t) 118 | 119 | var r0 *jwt.Token 120 | if rf, ok := ret.Get(0).(func(*http.Request, string) *jwt.Token); ok { 121 | r0 = rf(r, t) 122 | } else { 123 | if ret.Get(0) != nil { 124 | r0 = ret.Get(0).(*jwt.Token) 125 | } 126 | } 127 | 128 | var r1 error 129 | if rf, ok := ret.Get(1).(func(*http.Request, string) error); ok { 130 | r1 = rf(r, t) 131 | } else { 132 | r1 = ret.Error(1) 133 | } 134 | 135 | return r0, r1 136 | } 137 | 138 | // mockJwtParser is an autogenerated mock type for the jwtParser type 139 | type mockJwtParser struct { 140 | mock.Mock 141 | } 142 | 143 | // parse provides a mock function with given fields: _a0, _a1 144 | func (_m *mockJwtParser) parse(_a0 string, _a1 jwt.Keyfunc) (*jwt.Token, error) { 145 | ret := _m.Called(_a0, _a1) 146 | 147 | var r0 *jwt.Token 148 | if rf, ok := ret.Get(0).(func(string, jwt.Keyfunc) *jwt.Token); ok { 149 | r0 = rf(_a0, _a1) 150 | } else { 151 | if ret.Get(0) != nil { 152 | r0 = ret.Get(0).(*jwt.Token) 153 | } 154 | } 155 | 156 | var r1 error 157 | if rf, ok := ret.Get(1).(func(string, jwt.Keyfunc) error); ok { 158 | r1 = rf(_a0, _a1) 159 | } else { 160 | r1 = ret.Error(1) 161 | } 162 | 163 | return r0, r1 164 | } 165 | 166 | // mockConfigurationDecoder is an autogenerated mock type for the configurationDecoder type 167 | type mockConfigurationDecoder struct { 168 | mock.Mock 169 | } 170 | 171 | // decode provides a mock function with given fields: _a0 172 | func (_m *mockConfigurationDecoder) decode(_a0 io.Reader) (configuration, error) { 173 | ret := _m.Called(_a0) 174 | 175 | var r0 configuration 176 | if rf, ok := ret.Get(0).(func(io.Reader) configuration); ok { 177 | r0 = rf(_a0) 178 | } else { 179 | r0 = ret.Get(0).(configuration) 180 | } 181 | 182 | var r1 error 183 | if rf, ok := ret.Get(1).(func(io.Reader) error); ok { 184 | r1 = rf(_a0) 185 | } else { 186 | r1 = ret.Error(1) 187 | } 188 | 189 | return r0, r1 190 | } 191 | 192 | // mockHTTPGetter is an autogenerated mock type for the httpGetter type 193 | type mockHTTPGetter struct { 194 | mock.Mock 195 | } 196 | 197 | // get provides a mock function with given fields: r, url 198 | func (_m *mockHTTPGetter) get(r *http.Request, url string) (*http.Response, error) { 199 | ret := _m.Called(r, url) 200 | 201 | var r0 *http.Response 202 | if rf, ok := ret.Get(0).(func(*http.Request, string) *http.Response); ok { 203 | r0 = rf(r, url) 204 | } else { 205 | if ret.Get(0) != nil { 206 | r0 = ret.Get(0).(*http.Response) 207 | } 208 | } 209 | 210 | var r1 error 211 | if rf, ok := ret.Get(1).(func(*http.Request, string) error); ok { 212 | r1 = rf(r, url) 213 | } else { 214 | r1 = ret.Error(1) 215 | } 216 | 217 | return r0, r1 218 | } 219 | 220 | // mockJwksDecoder is an autogenerated mock type for the jwksDecoder type 221 | type mockJwksDecoder struct { 222 | mock.Mock 223 | } 224 | 225 | // decode provides a mock function with given fields: _a0 226 | func (_m *mockJwksDecoder) decode(_a0 io.Reader) (jose.JSONWebKeySet, error) { 227 | ret := _m.Called(_a0) 228 | 229 | var r0 jose.JSONWebKeySet 230 | if rf, ok := ret.Get(0).(func(io.Reader) jose.JSONWebKeySet); ok { 231 | r0 = rf(_a0) 232 | } else { 233 | r0 = ret.Get(0).(jose.JSONWebKeySet) 234 | } 235 | 236 | var r1 error 237 | if rf, ok := ret.Get(1).(func(io.Reader) error); ok { 238 | r1 = rf(_a0) 239 | } else { 240 | r1 = ret.Error(1) 241 | } 242 | 243 | return r0, r1 244 | } 245 | 246 | type mockPemEncoder struct { 247 | mock.Mock 248 | } 249 | 250 | // encode provides a mock function with given fields: key 251 | func (_m *mockPemEncoder) encode(key interface{}) ([]byte, error) { 252 | ret := _m.Called(key) 253 | 254 | var r0 []byte 255 | if rf, ok := ret.Get(0).(func(interface{}) []byte); ok { 256 | r0 = rf(key) 257 | } else { 258 | if ret.Get(0) != nil { 259 | r0 = ret.Get(0).([]byte) 260 | } 261 | } 262 | 263 | var r1 error 264 | if rf, ok := ret.Get(1).(func(interface{}) error); ok { 265 | r1 = rf(key) 266 | } else { 267 | r1 = ret.Error(1) 268 | } 269 | 270 | return r0, r1 271 | } 272 | 273 | // mockProvidersGetter is an autogenerated mock type for the providersGetter type 274 | type mockProvidersGetter struct { 275 | mock.Mock 276 | } 277 | 278 | // get provides a mock function with given fields: 279 | func (_m *mockProvidersGetter) get() ([]Provider, error) { 280 | ret := _m.Called() 281 | 282 | var r0 []Provider 283 | if rf, ok := ret.Get(0).(func() []Provider); ok { 284 | r0 = rf() 285 | } else { 286 | if ret.Get(0) != nil { 287 | r0 = ret.Get(0).([]Provider) 288 | } 289 | } 290 | 291 | var r1 error 292 | if rf, ok := ret.Get(1).(func() error); ok { 293 | r1 = rf() 294 | } else { 295 | r1 = ret.Error(1) 296 | } 297 | 298 | return r0, r1 299 | } 300 | 301 | // mockPemToRSAPublicKeyParser is an autogenerated mock type for the pemToRSAPublicKeyParser type 302 | type mockPemToRSAPublicKeyParser struct { 303 | mock.Mock 304 | } 305 | 306 | // parse provides a mock function with given fields: key 307 | func (_m *mockPemToRSAPublicKeyParser) parse(key []byte) (*rsa.PublicKey, error) { 308 | ret := _m.Called(key) 309 | 310 | var r0 *rsa.PublicKey 311 | if rf, ok := ret.Get(0).(func([]byte) *rsa.PublicKey); ok { 312 | r0 = rf(key) 313 | } else { 314 | if ret.Get(0) != nil { 315 | r0 = ret.Get(0).(*rsa.PublicKey) 316 | } 317 | } 318 | 319 | var r1 error 320 | if rf, ok := ret.Get(1).(func([]byte) error); ok { 321 | r1 = rf(key) 322 | } else { 323 | r1 = ret.Error(1) 324 | } 325 | 326 | return r0, r1 327 | } 328 | 329 | // mockSigningKeySetGetter is an autogenerated mock type for the signingKeySetGetter type 330 | type mockSigningKeySetGetter struct { 331 | mock.Mock 332 | } 333 | 334 | // get provides a mock function with given fields: r, issuer 335 | func (_m *mockSigningKeySetGetter) get(r *http.Request, issuer string) ([]signingKey, error) { 336 | ret := _m.Called(r, issuer) 337 | 338 | var r0 []signingKey 339 | if rf, ok := ret.Get(0).(func(*http.Request, string) []signingKey); ok { 340 | r0 = rf(r, issuer) 341 | } else { 342 | if ret.Get(0) != nil { 343 | r0 = ret.Get(0).([]signingKey) 344 | } 345 | } 346 | 347 | var r1 error 348 | if rf, ok := ret.Get(1).(func(*http.Request, string) error); ok { 349 | r1 = rf(r, issuer) 350 | } else { 351 | r1 = ret.Error(1) 352 | } 353 | 354 | return r0, r1 355 | } 356 | -------------------------------------------------------------------------------- /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 | // The GetProvidersFunc defines the function type used to retrieve the collection of allowed OP(s) along with the 18 | // respective client IDs registered with those providers that can access the backend service 19 | // using this package. 20 | // A function of this type must be provided to NewConfiguration through the option ProvidersGetter. 21 | // The given function will then be invoked for every request intercepted by the Authenticate or AuthenticateUser middleware. 22 | type GetProvidersFunc func() ([]Provider, error) 23 | 24 | type providersGetter interface { 25 | get() ([]Provider, error) 26 | } 27 | 28 | func (f GetProvidersFunc) get() ([]Provider, error) { 29 | return f() 30 | } 31 | 32 | // providers represent a collection of OPs. 33 | type providers []Provider 34 | 35 | // NewProvider returns a new instance of a Provider created with the given issuer and clientIDs. 36 | func NewProvider(issuer string, clientIDs []string) (Provider, error) { 37 | p := Provider{issuer, clientIDs} 38 | 39 | if err := p.validate(); err != nil { 40 | return Provider{}, err 41 | } 42 | 43 | return p, nil 44 | } 45 | 46 | func (ps providers) validate() error { 47 | if len(ps) == 0 { 48 | return &SetupError{ 49 | Code: SetupErrorEmptyProviderCollection, 50 | Message: "The collection of providers must contain at least one element.", 51 | } 52 | } 53 | 54 | for _, p := range ps { 55 | if err := p.validate(); err != nil { 56 | return err 57 | } 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func (p Provider) validate() error { 64 | if err := validateProviderIssuer(p.Issuer); err != nil { 65 | return err 66 | } 67 | 68 | return validateProviderClientIDs(p.ClientIDs) 69 | } 70 | 71 | func validateProviderIssuer(iss string) error { 72 | if iss == "" { 73 | return &SetupError{ 74 | Code: SetupErrorInvalidIssuer, 75 | Message: "Empty string issuer not allowed.", 76 | } 77 | } 78 | 79 | // TODO: Validate that the issuer format complies with openid spec. 80 | return nil 81 | } 82 | 83 | func validateProviderClientIDs(cIDs []string) error { 84 | if len(cIDs) == 0 { 85 | return &SetupError{ 86 | Code: SetupErrorInvalidClientIDs, 87 | Message: "At leat one client id must be provided.", 88 | } 89 | } 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /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, Provider{}} 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/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 | if h == "" { 20 | return h, &ValidationError{ 21 | Code: ValidationErrorAuthorizationHeaderNotFound, 22 | Message: "The 'Authorization' header was not found or was empty.", 23 | HTTPStatus: http.StatusBadRequest, 24 | } 25 | } 26 | 27 | p := strings.Split(h, " ") 28 | 29 | if len(p) != 2 { 30 | return h, &ValidationError{ 31 | Code: ValidationErrorAuthorizationHeaderWrongFormat, 32 | Message: "The 'Authorization' header did not have the correct format.", 33 | HTTPStatus: http.StatusBadRequest, 34 | } 35 | } 36 | 37 | if p[0] != "Bearer" { 38 | return h, &ValidationError{ 39 | Code: ValidationErrorAuthorizationHeaderWrongSchemeName, 40 | Message: "The 'Authorization' header scheme name was not 'Bearer'", 41 | HTTPStatus: http.StatusBadRequest, 42 | } 43 | } 44 | 45 | return p[1], nil 46 | } 47 | -------------------------------------------------------------------------------- /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 pemEncoder interface { 11 | encode(key interface{}) ([]byte, error) 12 | } 13 | 14 | type pemPublicKeyEncoder struct { 15 | } 16 | 17 | func (e *pemPublicKeyEncoder) encode(key interface{}) ([]byte, error) { 18 | mk, err := x509.MarshalPKIXPublicKey(key) 19 | if err != nil { 20 | return nil, &ValidationError{ 21 | Code: ValidationErrorMarshallingKey, 22 | Message: fmt.Sprint("The jwk key could not be marshalled."), 23 | Err: err, 24 | HTTPStatus: http.StatusInternalServerError, 25 | } 26 | } 27 | 28 | ed := pem.EncodeToMemory(&pem.Block{ 29 | Bytes: mk, 30 | }) 31 | 32 | return ed, nil 33 | } 34 | -------------------------------------------------------------------------------- /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 TestPemPublicKeyEncoder_Encode_ReturnsMarshallingKeyError(t *testing.T) { 13 | e := &pemPublicKeyEncoder{} 14 | _, err := e.encode(nil) 15 | 16 | if err == nil { 17 | t.Fatal("An error was expected but not returned.") 18 | } 19 | 20 | expectValidationError(t, err, ValidationErrorMarshallingKey, http.StatusInternalServerError, nil) 21 | 22 | } 23 | 24 | func TestPemPublicKeyEncoder_Encode_UsingRSAPublicKey(t *testing.T) { 25 | rsaKey := &rsa.PublicKey{N: big.NewInt(9871234), E: 15} 26 | 27 | e := &pemPublicKeyEncoder{} 28 | ek, err := e.encode(rsaKey) 29 | 30 | if err != nil { 31 | t.Error("An error was not expected but returned.") 32 | } 33 | 34 | if ek == nil { 35 | t.Error("The encoded key should not be nil.") 36 | } 37 | 38 | pBlock, rest := pem.Decode(ek) 39 | 40 | if pBlock == nil { 41 | t.Fatal("A pem block was not found in the encoded key.") 42 | } 43 | 44 | if len(rest) != 0 { 45 | t.Errorf("The encoded key was not fully pem decoded. Remaining buffer len %v.", len(rest)) 46 | } 47 | 48 | pub, err := x509.ParsePKIXPublicKey(pBlock.Bytes) 49 | 50 | if err != nil { 51 | t.Errorf("Parsing the key as DER public key returned the error %v.", err) 52 | } 53 | 54 | if pub == nil { 55 | t.Fatal("The key could not be parsed as a DER public key.") 56 | } 57 | 58 | if rpk, ok := pub.(*rsa.PublicKey); ok { 59 | rn := rpk.N.Int64() 60 | en := rsaKey.N.Int64() 61 | if en != rn { 62 | t.Error("Expected N", en, "but got", rn) 63 | } 64 | if rpk.E != rsaKey.E { 65 | t.Error("Expected E", rsaKey.E, "but got", rpk.E) 66 | } 67 | } else { 68 | t.Errorf("Expected public key type '*rsa.PublicKey' but got %T.", pub) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /openid/signingkeyprovider.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type signingKeyGetter interface { 9 | flushCachedSigningKeys(issuer string) error 10 | getSigningKey(r *http.Request, issuer string, kid string) ([]byte, error) 11 | } 12 | 13 | type signingKeyProvider struct { 14 | keySetGetter signingKeySetGetter 15 | jwksMap map[string][]signingKey 16 | } 17 | 18 | func newSigningKeyProvider(kg signingKeySetGetter) *signingKeyProvider { 19 | keyMap := make(map[string][]signingKey) 20 | return &signingKeyProvider{kg, keyMap} 21 | } 22 | 23 | func (s *signingKeyProvider) flushCachedSigningKeys(issuer string) error { 24 | delete(s.jwksMap, issuer) 25 | return nil 26 | } 27 | 28 | func (s *signingKeyProvider) refreshSigningKeys(r *http.Request, issuer string) error { 29 | skeys, err := s.keySetGetter.get(r, issuer) 30 | 31 | if err != nil { 32 | return err 33 | } 34 | 35 | s.jwksMap[issuer] = skeys 36 | return nil 37 | } 38 | 39 | func (s *signingKeyProvider) getSigningKey(r *http.Request, issuer string, kid string) ([]byte, error) { 40 | sk := findKey(s.jwksMap, issuer, kid) 41 | 42 | if sk != nil { 43 | return sk, nil 44 | } 45 | 46 | err := s.refreshSigningKeys(r, issuer) 47 | 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | sk = findKey(s.jwksMap, issuer, kid) 53 | 54 | if sk == nil { 55 | return nil, &ValidationError{ 56 | Code: ValidationErrorKidNotFound, 57 | Message: fmt.Sprintf("The jwk set retrieved for the issuer %v does not contain a key identifier %v.", issuer, kid), 58 | HTTPStatus: http.StatusUnauthorized, 59 | } 60 | } 61 | 62 | return sk, nil 63 | } 64 | 65 | func findKey(km map[string][]signingKey, issuer string, kid string) []byte { 66 | if skSet, ok := km[issuer]; ok { 67 | if kid == "" { 68 | return skSet[0].key 69 | } 70 | for _, sk := range skSet { 71 | if sk.keyID == kid { 72 | return sk.key 73 | } 74 | } 75 | } 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /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 | keyGetter.On("get", (*http.Request)(nil), iss).Return([]signingKey{{keyID: kid, key: []byte(key)}}, nil) 26 | 27 | // rk, re := keyCache.getSigningKey(nil, iss, kid) 28 | expectKey(t, keyCache, iss, kid, key) 29 | 30 | // Validate that the key is cached 31 | expectCachedKid(t, keyCache, iss, kid, key) 32 | 33 | keyGetter.AssertExpectations(t) 34 | } 35 | 36 | func Test_getSigningKey_WhenProviderReturnsError(t *testing.T) { 37 | keyGetter, keyCache := createSigningKeyProvider(t) 38 | 39 | iss := "issuer" 40 | kid := "kid1" 41 | ee := &ValidationError{Code: ValidationErrorGetJwksFailure, HTTPStatus: http.StatusUnauthorized} 42 | 43 | keyGetter.On("get", (*http.Request)(nil), iss).Return(nil, ee) 44 | 45 | rk, re := keyCache.getSigningKey(nil, iss, kid) 46 | 47 | expectValidationError(t, re, ee.Code, ee.HTTPStatus, nil) 48 | 49 | if rk != nil { 50 | t.Error("A key was returned but not expected") 51 | } 52 | 53 | cachedKeys := keyCache.jwksMap[iss] 54 | if len(cachedKeys) != 0 { 55 | t.Fatal("There shouldnt be cached keys for the targeted issuer.") 56 | } 57 | 58 | keyGetter.AssertExpectations(t) 59 | } 60 | 61 | func Test_getSigningKey_WhenKeyIsNotFound(t *testing.T) { 62 | keyGetter, keyCache := createSigningKeyProvider(t) 63 | 64 | iss := "issuer" 65 | kid := "kid1" 66 | tkid := "kid2" 67 | key := "signingKey" 68 | 69 | keyGetter.On("get", (*http.Request)(nil), iss).Return([]signingKey{{keyID: kid, key: []byte(key)}}, nil) 70 | 71 | rk, re := keyCache.getSigningKey(nil, iss, tkid) 72 | 73 | expectValidationError(t, re, ValidationErrorKidNotFound, http.StatusUnauthorized, nil) 74 | 75 | if rk != nil { 76 | t.Error("A key was returned but not expected") 77 | } 78 | 79 | expectCachedKid(t, keyCache, iss, kid, key) 80 | 81 | keyGetter.AssertExpectations(t) 82 | } 83 | 84 | func Test_flushCachedSigningKeys_FlushedKeysAreDeleted(t *testing.T) { 85 | _, keyCache := createSigningKeyProvider(t) 86 | 87 | iss := "issuer" 88 | iss2 := "issuer2" 89 | kid := "kid1" 90 | key := "signingKey" 91 | keyCache.jwksMap[iss] = []signingKey{{keyID: kid, key: []byte(key)}} 92 | keyCache.jwksMap[iss2] = []signingKey{{keyID: kid, key: []byte(key)}} 93 | 94 | keyCache.flushCachedSigningKeys(iss2) 95 | 96 | dk := keyCache.jwksMap[iss2] 97 | 98 | if dk != nil { 99 | t.Error("Flushed keys should not be in the cache.") 100 | } 101 | 102 | expectCachedKid(t, keyCache, iss, kid, key) 103 | } 104 | 105 | func Test_flushCachedSigningKey_RetrieveFlushedKey(t *testing.T) { 106 | keyGetter, keyCache := createSigningKeyProvider(t) 107 | 108 | iss := "issuer" 109 | kid := "kid1" 110 | key := "signingKey" 111 | 112 | keyGetter.On("get", (*http.Request)(nil), iss).Return([]signingKey{{keyID: kid, key: []byte(key)}}, nil).Twice() 113 | 114 | // Get the signing key not yet cached will cache it. 115 | expectKey(t, keyCache, iss, kid, key) 116 | 117 | // Flush the signing keys for the given provider. 118 | keyCache.flushCachedSigningKeys(iss) 119 | 120 | // Get the signing key will once again call the provider and cache the keys. 121 | 122 | expectKey(t, keyCache, iss, kid, key) 123 | 124 | // Validate that the key is cached 125 | expectCachedKid(t, keyCache, iss, kid, key) 126 | 127 | keyGetter.AssertExpectations(t) 128 | } 129 | 130 | func expectCachedKid(t *testing.T, keyProv *signingKeyProvider, iss string, kid string, key string) { 131 | 132 | cachedKeys := keyProv.jwksMap[iss] 133 | if len(cachedKeys) == 0 { 134 | t.Fatal("The keys were not cached as expected.") 135 | } 136 | 137 | foundKid := false 138 | for _, cachedKey := range cachedKeys { 139 | if cachedKey.keyID == kid { 140 | foundKid = true 141 | if keyStr := string(cachedKey.key); keyStr != key { 142 | t.Error("Expected key", key, "but got", keyStr) 143 | } 144 | 145 | continue 146 | } 147 | } 148 | 149 | if !foundKid { 150 | t.Error("A key with key id", kid, "was not found.") 151 | } 152 | } 153 | 154 | func expectKey(t *testing.T, c signingKeyGetter, iss string, kid string, key string) { 155 | sk, re := c.getSigningKey(nil, iss, kid) 156 | 157 | if re != nil { 158 | t.Error("An error was returned but not expected.") 159 | } 160 | 161 | if sk == nil { 162 | t.Fatal("The returned signing key should not be nil.") 163 | } 164 | 165 | keyStr := string(sk) 166 | 167 | if keyStr != key { 168 | t.Error("Expected key", key, "but got", keyStr) 169 | } 170 | } 171 | 172 | func createSigningKeyProvider(t *testing.T) (*mockSigningKeySetGetter, *signingKeyProvider) { 173 | mock := &mockSigningKeySetGetter{} 174 | return mock, newSigningKeyProvider(mock) 175 | } 176 | -------------------------------------------------------------------------------- /openid/signingkeysetprovider.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type signingKeySetGetter interface { 9 | get(r *http.Request, issuer string) ([]signingKey, error) 10 | } 11 | 12 | type signingKeySetProvider struct { 13 | configGetter configurationGetter 14 | jwksGetter jwksGetter 15 | keyEncoder pemEncoder 16 | } 17 | 18 | type signingKey struct { 19 | keyID string 20 | key []byte 21 | } 22 | 23 | func newSigningKeySetProvider(cg configurationGetter, jg jwksGetter, ke pemEncoder) *signingKeySetProvider { 24 | return &signingKeySetProvider{cg, jg, ke} 25 | } 26 | 27 | func (signProv *signingKeySetProvider) get(r *http.Request, iss string) ([]signingKey, error) { 28 | conf, err := signProv.configGetter.get(r, iss) 29 | 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | jwks, err := signProv.jwksGetter.get(r, conf.JwksURI) 35 | 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | if len(jwks.Keys) == 0 { 41 | return nil, &ValidationError{ 42 | Code: ValidationErrorEmptyJwk, 43 | Message: fmt.Sprintf("The jwk set retrieved for the issuer %v does not contain any key.", iss), 44 | HTTPStatus: http.StatusUnauthorized, 45 | } 46 | } 47 | 48 | sk := make([]signingKey, len(jwks.Keys)) 49 | 50 | for i, k := range jwks.Keys { 51 | ek, err := signProv.keyEncoder.encode(k.Key) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | sk[i] = signingKey{k.KeyID, ek} 57 | } 58 | 59 | return sk, nil 60 | } 61 | -------------------------------------------------------------------------------- /openid/signingkeysetprovider_test.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/mock" 10 | "gopkg.in/square/go-jose.v2" 11 | ) 12 | 13 | func TestSigningKeySetProvider_Get_WhenGetConfigurationReturnsError(t *testing.T) { 14 | configGetter, _, _, skProv := createSigningKeySetProvider(t) 15 | 16 | ee := &ValidationError{Code: ValidationErrorGetOpenIdConfigurationFailure, HTTPStatus: http.StatusUnauthorized} 17 | configGetter.On("get", mock.Anything).Return(configuration{}, ee) 18 | 19 | sk, re := skProv.get(nil, mock.Anything) 20 | 21 | expectValidationError(t, re, ee.Code, ee.HTTPStatus, nil) 22 | 23 | if sk != nil { 24 | t.Error("The returned signing keys should be nil") 25 | } 26 | 27 | configGetter.AssertExpectations(t) 28 | } 29 | 30 | func TestSigningKeySetProvider_Get_WhenGetJwksReturnsError(t *testing.T) { 31 | configGetter, jwksGetter, _, skProv := createSigningKeySetProvider(t) 32 | req := httptest.NewRequest(http.MethodGet, "/", nil) 33 | 34 | ee := &ValidationError{Code: ValidationErrorGetJwksFailure, HTTPStatus: http.StatusUnauthorized} 35 | 36 | jwksGetter.On("get", req, mock.Anything).Return(jose.JSONWebKeySet{}, ee) 37 | 38 | configGetter.On("get", mock.Anything).Return(configuration{}, nil) 39 | 40 | sk, re := skProv.get(req, mock.Anything) 41 | 42 | expectValidationError(t, re, ee.Code, ee.HTTPStatus, nil) 43 | 44 | if sk != nil { 45 | t.Error("The returned signing keys should be nil") 46 | } 47 | 48 | configGetter.AssertExpectations(t) 49 | jwksGetter.AssertExpectations(t) 50 | } 51 | 52 | func TestSigningKeySetProvider_Get_WhenJwkSetIsEmpty(t *testing.T) { 53 | configGetter, jwksGetter, _, skProv := createSigningKeySetProvider(t) 54 | 55 | ee := &ValidationError{Code: ValidationErrorEmptyJwk, HTTPStatus: http.StatusUnauthorized} 56 | 57 | jwksGetter.On("get", (*http.Request)(nil), mock.Anything).Return(jose.JSONWebKeySet{}, nil) 58 | configGetter.On("get", mock.Anything).Return(configuration{}, nil) 59 | 60 | sk, re := skProv.get(nil, mock.Anything) 61 | 62 | expectValidationError(t, re, ee.Code, ee.HTTPStatus, nil) 63 | 64 | if sk != nil { 65 | t.Error("The returned signing keys should be nil") 66 | } 67 | 68 | configGetter.AssertExpectations(t) 69 | jwksGetter.AssertExpectations(t) 70 | } 71 | 72 | func TestSigningKeySetProvider_Get_WhenKeyEncodingReturnsError(t *testing.T) { 73 | configGetter, jwksGetter, pemEncoder, skProv := createSigningKeySetProvider(t) 74 | 75 | ee := &ValidationError{Code: ValidationErrorMarshallingKey, HTTPStatus: http.StatusInternalServerError} 76 | ejwks := jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: nil}}} 77 | 78 | jwksGetter.On("get", (*http.Request)(nil), mock.Anything).Return(ejwks, nil) 79 | configGetter.On("get", mock.Anything).Return(configuration{}, nil) 80 | pemEncoder.On("encode", nil).Return(nil, ee) 81 | 82 | sk, re := skProv.get(nil, mock.Anything) 83 | 84 | expectValidationError(t, re, ee.Code, ee.HTTPStatus, nil) 85 | 86 | if sk != nil { 87 | t.Error("The returned signing keys should be nil") 88 | } 89 | 90 | configGetter.AssertExpectations(t) 91 | jwksGetter.AssertExpectations(t) 92 | pemEncoder.AssertExpectations(t) 93 | } 94 | 95 | func TestSigningKeySetProvider_Get_WhenKeyEncodingReturnsSuccess(t *testing.T) { 96 | configGetter, jwksGetter, pemEncoder, skProv := createSigningKeySetProvider(t) 97 | req := httptest.NewRequest(http.MethodGet, "/", nil) 98 | 99 | keys := make([]jose.JSONWebKey, 2) 100 | encryptedKeys := make([]signingKey, 2) 101 | 102 | for i := 0; i < cap(keys); i = i + 1 { 103 | keys[i] = jose.JSONWebKey{KeyID: fmt.Sprintf("%v", i), Key: i} 104 | encryptedKeys[i] = signingKey{keyID: fmt.Sprintf("%v", i), key: []byte(fmt.Sprintf("%v", i))} 105 | } 106 | 107 | ejwks := jose.JSONWebKeySet{Keys: keys} 108 | 109 | jwksGetter.On("get", req, mock.Anything).Return(ejwks, nil) 110 | configGetter.On("get", mock.Anything).Return(configuration{}, nil) 111 | 112 | for i, encryptedKey := range encryptedKeys { 113 | pemEncoder.On("encode", keys[i].Key).Return(encryptedKey.key, nil) 114 | } 115 | 116 | sk, re := skProv.get(req, mock.Anything) 117 | 118 | if re != nil { 119 | t.Error("An error was returned but not expected.") 120 | } 121 | 122 | if sk == nil { 123 | t.Fatal("The returned signing keys should be not nil") 124 | } 125 | 126 | if len(sk) != len(encryptedKeys) { 127 | t.Error("Returned", len(sk), "encrypted keys, but expected", len(encryptedKeys)) 128 | } 129 | 130 | for i, encryptedKey := range encryptedKeys { 131 | if encryptedKey.keyID != sk[i].keyID { 132 | t.Error("Key at", i, "should have keyID", encryptedKey.keyID, "but was", sk[i].keyID) 133 | } 134 | if string(encryptedKey.key) != string(sk[i].key) { 135 | t.Error("Key at", i, "should be", encryptedKey.key, "but was", sk[i].key) 136 | } 137 | } 138 | 139 | configGetter.AssertExpectations(t) 140 | jwksGetter.AssertExpectations(t) 141 | pemEncoder.AssertExpectations(t) 142 | } 143 | 144 | func createSigningKeySetProvider(t *testing.T) (*mockConfigurationGetter, *mockJwksGetter, *mockPemEncoder, signingKeySetProvider) { 145 | configGetter := &mockConfigurationGetter{} 146 | jwksGetter := &mockJwksGetter{} 147 | pemEncoder := &mockPemEncoder{} 148 | 149 | skProv := signingKeySetProvider{configGetter: configGetter, jwksGetter: jwksGetter, keyEncoder: pemEncoder} 150 | return configGetter, jwksGetter, pemEncoder, skProv 151 | } 152 | -------------------------------------------------------------------------------- /openid/user.go: -------------------------------------------------------------------------------- 1 | package openid 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/dgrijalva/jwt-go" 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{ 25 | Code: ValidationErrorIdTokenEmpty, 26 | Message: "The token provided to created a user was nil.", 27 | HTTPStatus: http.StatusUnauthorized, 28 | } 29 | } 30 | 31 | iss := getIssuer(t).(string) 32 | 33 | if iss == "" { 34 | return nil, &ValidationError{ 35 | Code: ValidationErrorInvalidIssuer, 36 | Message: "The token provided to created a user did not contain a valid 'iss' claim", 37 | HTTPStatus: http.StatusInternalServerError, 38 | } 39 | } 40 | 41 | sub := getSubject(t).(string) 42 | 43 | if sub == "" { 44 | return nil, &ValidationError{ 45 | Code: ValidationErrorInvalidSubject, 46 | Message: "The token provided to created a user did not contain a valid 'sub' claim.", 47 | HTTPStatus: http.StatusInternalServerError, 48 | } 49 | 50 | } 51 | 52 | u := new(User) 53 | u.Issuer = iss 54 | u.ID = sub 55 | u.Claims = t.Claims.(jwt.MapClaims) 56 | return u, nil 57 | } 58 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------