From 45230569d35d5d445d6f8bde5dff5d0f79913fd3 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 14 Sep 2020 07:52:16 +0200 Subject: [PATCH] change RP interfaces --- pkg/cli/cli.go | 30 +-- pkg/oidc/verifier.go | 11 ++ pkg/op/op.go | 9 +- pkg/rp/default_rp.go | 172 ++++------------ pkg/rp/default_verifier.go | 14 +- pkg/rp/relaying_party.go | 390 +++++++++++++++++++++++++++++++++---- pkg/rp/tockenexchange.go | 14 +- pkg/rp/verifier.go | 136 ++++++------- pkg/utils/hash.go | 3 +- pkg/utils/http.go | 9 +- 10 files changed, 529 insertions(+), 259 deletions(-) diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index c7328e4..418aaa6 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -3,20 +3,22 @@ package cli import ( "context" "fmt" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" - "github.com/caos/oidc/pkg/utils" - "github.com/google/uuid" - "github.com/sirupsen/logrus" "log" "net/http" "strings" "time" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/rp" + "github.com/caos/oidc/pkg/utils" ) -func CodeFlow(rpc *rp.Config, key []byte, callbackPath string, port string) *oidc.Tokens { +func CodeFlow(rpc *rp.Configuration, key []byte, callbackPath string, port string) *oidc.Tokens { cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - provider, err := rp.NewDefaultRP(rpc, rp.WithCookieHandler(cookieHandler)) //rp.WithPKCE(cookieHandler)) //, + provider, err := rp.NewRelayingParty(rpc, rp.WithCookieHandler(cookieHandler)) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } @@ -24,9 +26,9 @@ func CodeFlow(rpc *rp.Config, key []byte, callbackPath string, port string) *oid return codeFlow(provider, callbackPath, port) } -func TokenForClient(rpc *rp.Config, key []byte, token *oidc.Tokens) *http.Client { +func TokenForClient(rpc *rp.Configuration, key []byte, token *oidc.Tokens) *http.Client { cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - provider, err := rp.NewDefaultRP(rpc, rp.WithCookieHandler(cookieHandler)) //rp.WithPKCE(cookieHandler)) //, + provider, err := rp.NewRelayingParty(rpc, rp.WithCookieHandler(cookieHandler)) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } @@ -34,9 +36,9 @@ func TokenForClient(rpc *rp.Config, key []byte, token *oidc.Tokens) *http.Client return provider.Client(context.Background(), token.Token) } -func CodeFlowForClient(rpc *rp.Config, key []byte, callbackPath string, port string) *http.Client { +func CodeFlowForClient(rpc *rp.Configuration, key []byte, callbackPath string, port string) *http.Client { cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - provider, err := rp.NewDefaultRP(rpc, rp.WithCookieHandler(cookieHandler)) //rp.WithPKCE(cookieHandler)) //, + provider, err := rp.NewRelayingParty(rpc, rp.WithCookieHandler(cookieHandler)) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } @@ -45,7 +47,7 @@ func CodeFlowForClient(rpc *rp.Config, key []byte, callbackPath string, port str return provider.Client(context.Background(), token.Token) } -func codeFlow(provider rp.DelegationTokenExchangeRP, callbackPath string, port string) *oidc.Tokens { +func codeFlow(provider rp.RelayingParty, callbackPath string, port string) *oidc.Tokens { loginPath := "/login" portStr := port if !strings.HasPrefix(port, ":") { @@ -55,12 +57,12 @@ func codeFlow(provider rp.DelegationTokenExchangeRP, callbackPath string, port s getToken, setToken := getAndSetTokens() state := uuid.New().String() - http.Handle(loginPath, provider.AuthURLHandler(state)) + http.Handle(loginPath, rp.AuthURLHandler(state, provider)) marshal := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) { setToken(w, tokens) } - http.Handle(callbackPath, provider.CodeExchangeHandler(marshal)) + http.Handle(callbackPath, rp.CodeExchangeHandler(marshal, provider)) // start http-server stopHttpServer := startHttpServer(portStr) diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index 47012ad..fb64947 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -72,6 +72,17 @@ type verifierConfig struct { //ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim type ACRVerifier func(string) error +//DefaultACRVerifier implements `ACRVerifier` returning an error +//if non of the provided values matches the acr claim +func DefaultACRVerifier(possibleValues []string) ACRVerifier { + return func(acr string) error { + if !utils.Contains(possibleValues, acr) { + return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr) + } + return nil + } +} + func DecryptToken(tokenString string) (string, error) { return tokenString, nil //TODO: impl } diff --git a/pkg/op/op.go b/pkg/op/op.go index 153e015..0d6f019 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/mux" "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" ) const ( @@ -16,8 +17,12 @@ const ( type OpenIDProvider interface { Configuration - Authorizer - SessionEnder + Storage() Storage + Decoder() utils.Decoder + Encoder() utils.Encoder + IDTokenVerifier() IDTokenHintVerifier + Crypto() Crypto + DefaultLogoutRedirectURI() string Signer() Signer Probes() []ProbesFn HttpHandler() http.Handler diff --git a/pkg/rp/default_rp.go b/pkg/rp/default_rp.go index f701ec1..610507c 100644 --- a/pkg/rp/default_rp.go +++ b/pkg/rp/default_rp.go @@ -2,12 +2,9 @@ package rp import ( "context" - "encoding/base64" "net/http" "strings" - "github.com/caos/oidc/pkg/oidc/grants" - "golang.org/x/oauth2" "github.com/caos/oidc/pkg/oidc" @@ -21,12 +18,7 @@ const ( pkceCode = "pkce" ) -var ( - DefaultErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) { - http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError) - } -) - +//deprecated: use NewRelayingParty instead //DefaultRP implements the `DelegationTokenExchangeRP` interface extending the `RelayingParty` interface type DefaultRP struct { endpoints Endpoints @@ -45,6 +37,36 @@ type DefaultRP struct { onlyOAuth2 bool } +func (p *DefaultRP) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { + return p.errorHandler +} + +func (p *DefaultRP) OAuthConfig() *oauth2.Config { + return &p.oauthConfig +} + +func (p *DefaultRP) IsPKCE() bool { + return p.pkce +} + +func (p *DefaultRP) CookieHandler() *utils.CookieHandler { + return p.cookieHandler +} + +func (p *DefaultRP) HttpClient() *http.Client { + return p.httpClient +} + +func (p *DefaultRP) IsOAuth2Only() bool { + return p.onlyOAuth2 +} + +func (p *DefaultRP) IDTokenVerifier() IDTokenVerifier { + return p.idTokenVerifier +} + +//deprecated: use NewRelayingParty instead +// //NewDefaultRP creates `DefaultRP` with the given //Config and possible configOptions //it will run discovery on the provided issuer @@ -89,6 +111,7 @@ func NewDefaultRP(rpConfig *Config, rpOpts ...DefaultRPOpts) (DelegationTokenExc //DefaultRPOpts is the type for providing dynamic options to the DefaultRP type DefaultRPOpts func(p *DefaultRP) +/* //WithCookieHandler set a `CookieHandler` for securing the various redirects func WithCookieHandler(cookieHandler *utils.CookieHandler) DefaultRPOpts { return func(p *DefaultRP) { @@ -118,108 +141,34 @@ func WithVerifierOpts(opts ...ConfFunc) DefaultRPOpts { p.verifierOpts = opts } } +*/ //AuthURL is the `RelayingParty` interface implementation //wrapping the oauth2 `AuthCodeURL` //returning the url of the auth request func (p *DefaultRP) AuthURL(state string, opts ...AuthURLOpt) string { - authOpts := make([]oauth2.AuthCodeOption, 0) - for _, opt := range opts { - authOpts = append(authOpts, opt()...) - } - return p.oauthConfig.AuthCodeURL(state, authOpts...) + return AuthURL(state, p, opts...) } //AuthURL is the `RelayingParty` interface implementation //extending the `AuthURL` method with a http redirect handler func (p *DefaultRP) AuthURLHandler(state string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - opts := make([]AuthURLOpt, 0) - if err := p.trySetStateCookie(w, state); err != nil { - http.Error(w, "failed to create state cookie: "+err.Error(), http.StatusUnauthorized) - return - } - if p.pkce { - codeChallenge, err := p.generateAndStoreCodeChallenge(w) - if err != nil { - http.Error(w, "failed to create code challenge: "+err.Error(), http.StatusUnauthorized) - return - } - opts = append(opts, WithCodeChallenge(codeChallenge)) - } - http.Redirect(w, r, p.AuthURL(state, opts...), http.StatusFound) - } -} - -func (p *DefaultRP) generateAndStoreCodeChallenge(w http.ResponseWriter) (string, error) { - var codeVerifier string - codeVerifier = "s" - if err := p.cookieHandler.SetCookie(w, pkceCode, codeVerifier); err != nil { - return "", err - } - return oidc.NewSHACodeChallenge(codeVerifier), nil + return AuthURLHandler(state, p) } +//deprecated: Use CodeExchange func and provide a RelayingParty +// //AuthURL is the `RelayingParty` interface implementation //handling the oauth2 code exchange, extracting and validating the id_token -//returning it paresed together with the oauth2 tokens (access, refresh) +//returning it parsed together with the oauth2 tokens (access, refresh) func (p *DefaultRP) CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { - ctx = context.WithValue(ctx, oauth2.HTTPClient, p.httpClient) - codeOpts := make([]oauth2.AuthCodeOption, 0) - for _, opt := range opts { - codeOpts = append(codeOpts, opt()...) - } - - token, err := p.oauthConfig.Exchange(ctx, code, codeOpts...) - if err != nil { - return nil, err //TODO: our error - } - idTokenString, ok := token.Extra(idTokenKey).(string) - if !ok { - //TODO: implement - } - - idToken := new(oidc.IDTokenClaims) - if !p.onlyOAuth2 { - idToken, err = VerifyTokens(ctx, token.AccessToken, idTokenString, p.idTokenVerifier) - if err != nil { - return nil, err //TODO: err - } - } - - return &oidc.Tokens{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil + return CodeExchange(ctx, code, p, opts...) } //AuthURL is the `RelayingParty` interface implementation //extending the `CodeExchange` method with callback function func (p *DefaultRP) CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - state, err := p.tryReadStateCookie(w, r) - if err != nil { - http.Error(w, "failed to get state: "+err.Error(), http.StatusUnauthorized) - return - } - params := r.URL.Query() - if params.Get("error") != "" { - p.errorHandler(w, r, params.Get("error"), params.Get("error_description"), state) - return - } - codeOpts := make([]CodeExchangeOpt, 0) - if p.pkce { - codeVerifier, err := p.cookieHandler.CheckCookie(r, pkceCode) - if err != nil { - http.Error(w, "failed to get code verifier: "+err.Error(), http.StatusUnauthorized) - return - } - codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) - } - tokens, err := p.CodeExchange(r.Context(), params.Get("code"), codeOpts...) - if err != nil { - http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) - return - } - callback(w, r, tokens, state) - } + return CodeExchangeHandler(callback, p) } // func (p *DefaultRP) Introspect(ctx context.Context, accessToken string) (oidc.TokenIntrospectResponse, error) { @@ -237,19 +186,19 @@ func (p *DefaultRP) Userinfo() {} //ClientCredentials is the `RelayingParty` interface implementation //handling the oauth2 client credentials grant func (p *DefaultRP) ClientCredentials(ctx context.Context, scopes ...string) (newToken *oauth2.Token, err error) { - return p.callTokenEndpoint(grants.ClientCredentialsGrantBasic(scopes...)) + return ClientCredentials(ctx, p, scopes...) } //TokenExchange is the `TokenExchangeRP` interface implementation //handling the oauth2 token exchange (draft) func (p *DefaultRP) TokenExchange(ctx context.Context, request *grants_tx.TokenExchangeRequest) (newToken *oauth2.Token, err error) { - return p.callTokenEndpoint(request) + return TokenExchange(ctx, request, p) } //DelegationTokenExchange is the `TokenExchangeRP` interface implementation //handling the oauth2 token exchange for a delegation token (draft) func (p *DefaultRP) DelegationTokenExchange(ctx context.Context, subjectToken string, reqOpts ...grants_tx.TokenExchangeOption) (newToken *oauth2.Token, err error) { - return p.TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...)) + return TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), p) } func (p *DefaultRP) discover() error { @@ -278,41 +227,6 @@ func (p *DefaultRP) getOAuthConfig(endpoint oauth2.Endpoint) oauth2.Config { } } -func (p *DefaultRP) callTokenEndpoint(request interface{}) (newToken *oauth2.Token, err error) { - req, err := utils.FormRequest(p.endpoints.TokenURL, request) - if err != nil { - return nil, err - } - auth := base64.StdEncoding.EncodeToString([]byte(p.config.ClientID + ":" + p.config.ClientSecret)) - req.Header.Set("Authorization", "Basic "+auth) - token := new(oauth2.Token) - if err := utils.HttpRequest(p.httpClient, req, token); err != nil { - return nil, err - } - return token, nil -} - -func (p *DefaultRP) trySetStateCookie(w http.ResponseWriter, state string) error { - if p.cookieHandler != nil { - if err := p.cookieHandler.SetCookie(w, stateParam, state); err != nil { - return err - } - } - return nil -} - -func (p *DefaultRP) tryReadStateCookie(w http.ResponseWriter, r *http.Request) (state string, err error) { - if p.cookieHandler == nil { - return r.FormValue(stateParam), nil - } - state, err = p.cookieHandler.CheckQueryCookie(r, stateParam) - if err != nil { - return "", err - } - p.cookieHandler.DeleteCookie(w, stateParam) - return state, nil -} - func (p *DefaultRP) Client(ctx context.Context, token *oauth2.Token) *http.Client { return p.oauthConfig.Client(ctx, token) } diff --git a/pkg/rp/default_verifier.go b/pkg/rp/default_verifier.go index c13c135..d8895ce 100644 --- a/pkg/rp/default_verifier.go +++ b/pkg/rp/default_verifier.go @@ -2,13 +2,12 @@ package rp import ( "context" - "fmt" "time" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" ) +//deprecated: use IDTokenVerifier or oidc.Verifier interfaces //DefaultVerifier implements the `Verifier` interface type DefaultVerifier struct { config *verifierConfig @@ -18,6 +17,7 @@ type DefaultVerifier struct { //ConfFunc is the type for providing dynamic options to the DefaultVerifier type ConfFunc func(*verifierConfig) +//deprecated: use NewIDTokenVerifier //NewDefaultVerifier creates `DefaultVerifier` with the given //issuer, clientID, keyset and possible configOptions func NewDefaultVerifier(issuer, clientID string, keySet oidc.KeySet, confOpts ...ConfFunc) Verifier { @@ -123,17 +123,14 @@ type iatConfig struct { maxAge time.Duration } +//deprecated: use oidc.DefaultACRVerifier directly //DefaultACRVerifier implements `ACRVerifier` returning an error //if non of the provided values matches the acr claim func DefaultACRVerifier(possibleValues []string) oidc.ACRVerifier { - return func(acr string) error { - if !utils.Contains(possibleValues, acr) { - return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr) - } - return nil - } + return oidc.DefaultACRVerifier(possibleValues) } +//deprecated: use VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (*oidc.IDTokenClaims, error) instead //Verify implements the `Verify` method of the `Verifier` interface //according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation //and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation @@ -142,6 +139,7 @@ func (v *DefaultVerifier) Verify(ctx context.Context, accessToken, idTokenString return VerifyTokens(ctx, accessToken, idTokenString, v) } +//deprecated: use VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (*oidc.IDTokenClaims, error) instead //Verify implements the `VerifyIDToken` method of the `Verifier` interface //according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation func (v *DefaultVerifier) VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) { diff --git a/pkg/rp/relaying_party.go b/pkg/rp/relaying_party.go index 8aba443..0192a35 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/rp/relaying_party.go @@ -3,65 +3,364 @@ package rp import ( "context" "net/http" + "strings" "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/oidc/grants" + "github.com/caos/oidc/pkg/utils" "golang.org/x/oauth2" ) //RelayingParty declares the minimal interface for oidc clients type RelayingParty interface { + //OAuthConfig returns the oauth2 Config + OAuthConfig() *oauth2.Config + + //IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)` + IsPKCE() bool + + //CookieHandler returns a http cookie handler used for various state transfer cookies + CookieHandler() *utils.CookieHandler + //Client return a standard http client where the token can be used Client(ctx context.Context, token *oauth2.Token) *http.Client - //AuthURL returns the authorization endpoint with a given state - AuthURL(state string, opts ...AuthURLOpt) string + /* + //AuthURL returns the authorization endpoint with a given state + AuthURL(state string, opts ...AuthURLOpt) string - //AuthURLHandler should implement the AuthURL func as http.HandlerFunc - //(redirecting to the auth endpoint) - AuthURLHandler(state string) http.HandlerFunc + //AuthURLHandler should implement the AuthURL func as http.HandlerFunc + //(redirecting to the auth endpoint) + AuthURLHandler(state string) http.HandlerFunc - //CodeExchange implements the OIDC Token Request (oauth2 Authorization Code Grant) - //returning an `Access Token` and `ID Token Claims` - CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (*oidc.Tokens, error) + //CodeExchange implements the OIDC Token Request (oauth2 Authorization Code Grant) + //returning an `Access Token` and `ID Token Claims` + CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (*oidc.Tokens, error) - //CodeExchangeHandler extends the CodeExchange func, - //calling the provided callback func on success with additional returned `state` - CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc + //CodeExchangeHandler extends the CodeExchange func, + //calling the provided callback func on success with additional returned `state` + CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc - //ClientCredentials implements the oauth2 Client Credentials Grant - //requesting an `Access Token` for the client itself, without user context - ClientCredentials(ctx context.Context, scopes ...string) (*oauth2.Token, error) + //ClientCredentials implements the oauth2 Client Credentials Grant + //requesting an `Access Token` for the client itself, without user context + ClientCredentials(ctx context.Context, scopes ...string) (*oauth2.Token, error) - //Introspects calls the Introspect Endpoint - //for validating an (access) token - // Introspect(ctx context.Context, token string) (TokenIntrospectResponse, error) + //Introspects calls the Introspect Endpoint + //for validating an (access) token + // Introspect(ctx context.Context, token string) (TokenIntrospectResponse, error) - //Userinfo implements the OIDC Userinfo call - //returning the info of the user for the requested scopes of an access token - Userinfo() + //Userinfo implements the OIDC Userinfo call + //returning the info of the user for the requested scopes of an access token + Userinfo() + */ + HttpClient() *http.Client + IsOAuth2Only() bool + IDTokenVerifier() IDTokenVerifier + ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) } -//PasswortGrantRP extends the `RelayingParty` interface with the oauth2 `Password Grant` // -//This interface is separated from the standard `RelayingParty` interface as the `password grant` -//is part of the oauth2 and therefore OIDC specification, but should only be used when there's no -//other possibility, so IMHO never ever. Ever. -type PasswortGrantRP interface { - RelayingParty +////PasswortGrantRP extends the `RelayingParty` interface with the oauth2 `Password Grant` +//// +////This interface is separated from the standard `RelayingParty` interface as the `password grant` +////is part of the oauth2 and therefore OIDC specification, but should only be used when there's no +////other possibility, so IMHO never ever. Ever. +//type PasswortGrantRP interface { +// RelayingParty +// +// //PasswordGrant implements the oauth2 `Password Grant`, +// //requesting an access token with the users `username` and `password` +// PasswordGrant(context.Context, string, string) (*oauth2.Token, error) +//} - //PasswordGrant implements the oauth2 `Password Grant`, - //requesting an access token with the users `username` and `password` - PasswordGrant(context.Context, string, string) (*oauth2.Token, error) +var ( + DefaultErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) { + http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError) + } +) + +type relayingParty struct { + endpoints Endpoints + + oauthConfig *oauth2.Config + config *Configuration + pkce bool + + httpClient *http.Client + cookieHandler *utils.CookieHandler + + errorHandler func(http.ResponseWriter, *http.Request, string, string, string) + + idTokenVerifier IDTokenVerifier + verifierOpts []ConfFunc + oauth2Only bool } -type Config struct { - ClientID string - ClientSecret string - CallbackURL string - Issuer string - Scopes []string - Endpoints oauth2.Endpoint +func (rp *relayingParty) OAuthConfig() *oauth2.Config { + return rp.oauthConfig +} + +func (rp *relayingParty) IsPKCE() bool { + return rp.pkce +} + +func (rp *relayingParty) CookieHandler() *utils.CookieHandler { + return rp.cookieHandler +} + +func (rp *relayingParty) HttpClient() *http.Client { + return rp.httpClient +} + +func (rp *relayingParty) IsOAuth2Only() bool { + return rp.oauth2Only +} + +func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier { + return rp.idTokenVerifier +} + +func (rp *relayingParty) Client(ctx context.Context, token *oauth2.Token) *http.Client { + panic("implement me") +} + +func (rp *relayingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { + return rp.errorHandler +} + +//NewRelayingParty creates a DelegationTokenExchangeRP with the given +//Config and possible configOptions +//it will run discovery on the provided issuer if AuthURL and TokenURL are not set +//if no verifier is provided using the options the `DefaultVerifier` is set +func NewRelayingParty(config *Configuration, options ...Option) (RelayingParty, error) { + isOpenID := isOpenID(config.Scopes) + + rp := &relayingParty{ + config: config, + httpClient: utils.DefaultHTTPClient, + oauth2Only: !isOpenID, + } + + for _, optFunc := range options { + optFunc(rp) + } + + rp.oauthConfig = config.Config + if config.Endpoint.AuthURL != "" && config.Endpoint.TokenURL != "" { + rp.oauthConfig = config.Config + } else { + endpoints, err := Discover(config.Issuer, rp.httpClient) + if err != nil { + return nil, err + } + rp.oauthConfig.Endpoint = endpoints.Endpoint + } + + if rp.errorHandler == nil { + rp.errorHandler = DefaultErrorHandler + } + + if isOpenID && rp.idTokenVerifier == nil { + rp.idTokenVerifier = NewIDTokenVerifier(config.Issuer, config.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL)) + } + + return rp, nil +} + +//DefaultRPOpts is the type for providing dynamic options to the DefaultRP +type Option func(*relayingParty) + +//WithCookieHandler set a `CookieHandler` for securing the various redirects +func WithCookieHandler(cookieHandler *utils.CookieHandler) Option { + return func(rp *relayingParty) { + rp.cookieHandler = cookieHandler + } +} + +//WithPKCE sets the RP to use PKCE (oauth2 code challenge) +//it also sets a `CookieHandler` for securing the various redirects +//and exchanging the code challenge +func WithPKCE(cookieHandler *utils.CookieHandler) Option { + return func(rp *relayingParty) { + rp.pkce = true + rp.cookieHandler = cookieHandler + } +} + +//WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier +func WithHTTPClient(client *http.Client) Option { + return func(rp *relayingParty) { + rp.httpClient = client + } +} + +func WithVerifierOpts(opts ...ConfFunc) Option { + return func(rp *relayingParty) { + rp.verifierOpts = opts + } +} + +//Discover calls the discovery endpoint of the provided issuer and returns the found endpoints +func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { + wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint + req, err := http.NewRequest("GET", wellKnown, nil) + if err != nil { + return Endpoints{}, err + } + discoveryConfig := new(oidc.DiscoveryConfiguration) + err = utils.HttpRequest(httpClient, req, &discoveryConfig) + if err != nil { + return Endpoints{}, err + } + return GetEndpoints(discoveryConfig), nil +} + +//AuthURL returns the auth request url +//(wrapping the oauth2 `AuthCodeURL`) +func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string { + authOpts := make([]oauth2.AuthCodeOption, 0) + for _, opt := range opts { + authOpts = append(authOpts, opt()...) + } + return rp.OAuthConfig().AuthCodeURL(state, authOpts...) +} + +//AuthURLHandler extends the `AuthURL` method with a http redirect handler +//including handling setting cookie for secure `state` transfer +func AuthURLHandler(state string, rp RelayingParty) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + opts := make([]AuthURLOpt, 0) + if err := trySetStateCookie(w, state, rp); err != nil { + http.Error(w, "failed to create state cookie: "+err.Error(), http.StatusUnauthorized) + return + } + if rp.IsPKCE() { + codeChallenge, err := GenerateAndStoreCodeChallenge(w, rp) + if err != nil { + http.Error(w, "failed to create code challenge: "+err.Error(), http.StatusUnauthorized) + return + } + opts = append(opts, WithCodeChallenge(codeChallenge)) + } + http.Redirect(w, r, AuthURL(state, rp, opts...), http.StatusFound) + } +} + +func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (string, error) { + var codeVerifier string + codeVerifier = "s" + if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil { + return "", err + } + return oidc.NewSHACodeChallenge(codeVerifier), nil +} + +//AuthURL is the `RelayingParty` interface implementation +//handling the oauth2 code exchange, extracting and validating the id_token +//returning it paresed together with the oauth2 tokens (access, refresh) +func CodeExchange(ctx context.Context, code string, rp RelayingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { + ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) + codeOpts := make([]oauth2.AuthCodeOption, 0) + for _, opt := range opts { + codeOpts = append(codeOpts, opt()...) + } + + token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...) + if err != nil { + return nil, err //TODO: our error + } + idTokenString, ok := token.Extra(idTokenKey).(string) + if !ok { + //TODO: implement + } + + idToken := new(oidc.IDTokenClaims) + if !rp.IsOAuth2Only() { + idToken, err = VerifyTokens(ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier()) + if err != nil { + return nil, err //TODO: err + } + } + + return &oidc.Tokens{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil +} + +//AuthURL is the `RelayingParty` interface implementation +//extending the `CodeExchange` method with callback function +func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelayingParty) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + state, err := tryReadStateCookie(w, r, rp) + if err != nil { + http.Error(w, "failed to get state: "+err.Error(), http.StatusUnauthorized) + return + } + params := r.URL.Query() + if params.Get("error") != "" { + rp.ErrorHandler()(w, r, params.Get("error"), params.Get("error_description"), state) + return + } + codeOpts := make([]CodeExchangeOpt, 0) + if rp.IsPKCE() { + codeVerifier, err := rp.CookieHandler().CheckCookie(r, pkceCode) + if err != nil { + http.Error(w, "failed to get code verifier: "+err.Error(), http.StatusUnauthorized) + return + } + codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) + } + tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...) + if err != nil { + http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) + return + } + callback(w, r, tokens, state) + } +} + +//ClientCredentials is the `RelayingParty` interface implementation +//handling the oauth2 client credentials grant +func ClientCredentials(ctx context.Context, rp RelayingParty, scopes ...string) (newToken *oauth2.Token, err error) { + return CallTokenEndpoint(grants.ClientCredentialsGrantBasic(scopes...), rp) +} + +func CallTokenEndpoint(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { + config := rp.OAuthConfig() + req, err := utils.FormRequest(rp.OAuthConfig().Endpoint.TokenURL, request, config.ClientID, config.ClientSecret, config.Endpoint.AuthStyle != oauth2.AuthStyleInParams) + if err != nil { + return nil, err + } + token := new(oauth2.Token) + if err := utils.HttpRequest(rp.HttpClient(), req, token); err != nil { + return nil, err + } + return token, nil +} + +func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) error { + if rp.CookieHandler() != nil { + if err := rp.CookieHandler().SetCookie(w, stateParam, state); err != nil { + return err + } + } + return nil +} + +func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty) (state string, err error) { + if rp.CookieHandler() == nil { + return r.FormValue(stateParam), nil + } + state, err = rp.CookieHandler().CheckQueryCookie(r, stateParam) + if err != nil { + return "", err + } + rp.CookieHandler().DeleteCookie(w, stateParam) + return state, nil +} + +type Configuration struct { + Issuer string + *oauth2.Config } type OptionFunc func(RelayingParty) @@ -106,3 +405,22 @@ func WithCodeVerifier(codeVerifier string) CodeExchangeOpt { return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)} } } + +func isOpenID(scopes []string) bool { + for _, scope := range scopes { + if scope == oidc.ScopeOpenID { + return true + } + } + return false +} + +//deprecated: use Configuration instead +type Config struct { + ClientID string + ClientSecret string + CallbackURL string + Issuer string + Scopes []string + Endpoints oauth2.Endpoint +} diff --git a/pkg/rp/tockenexchange.go b/pkg/rp/tockenexchange.go index d84b38e..29929b4 100644 --- a/pkg/rp/tockenexchange.go +++ b/pkg/rp/tockenexchange.go @@ -12,7 +12,7 @@ import ( type TokenExchangeRP interface { RelayingParty - //TokenExchange implement the `Token Echange Grant` exchanging some token for an other + //TokenExchange implement the `Token Exchange Grant` exchanging some token for an other TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error) } @@ -25,3 +25,15 @@ type DelegationTokenExchangeRP interface { //providing an access token in request for a `delegation` token for a given resource / audience DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error) } + +//TokenExchange is the `TokenExchangeRP` interface implementation +//handling the oauth2 token exchange (draft) +func TokenExchange(ctx context.Context, request *tokenexchange.TokenExchangeRequest, rp RelayingParty) (newToken *oauth2.Token, err error) { + return CallTokenEndpoint(request, rp) +} + +//DelegationTokenExchange is the `TokenExchangeRP` interface implementation +//handling the oauth2 token exchange for a delegation token (draft) +func DelegationTokenExchange(ctx context.Context, subjectToken string, rp RelayingParty, reqOpts ...tokenexchange.TokenExchangeOption) (newToken *oauth2.Token, err error) { + return TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), rp) +} diff --git a/pkg/rp/verifier.go b/pkg/rp/verifier.go index c98134e..3545cf6 100644 --- a/pkg/rp/verifier.go +++ b/pkg/rp/verifier.go @@ -9,16 +9,6 @@ import ( "github.com/caos/oidc/pkg/oidc" ) -//deprecated: Use IDTokenVerifier or oidc.Verifier -type Verifier interface { - - //Verify checks the access_token and id_token and returns the `id token claims` - Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) - - //VerifyIDToken checks the id_token only and returns its `id token claims` - VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) -} - type IDTokenVerifier interface { oidc.Verifier ClientID() string @@ -29,63 +19,6 @@ type IDTokenVerifier interface { MaxAge() time.Duration } -type idTokenVerifier struct { - issuer string - maxAgeIAT time.Duration - offset time.Duration - clientID string - supportedSignAlgs []string - keySet oidc.KeySet - acr oidc.ACRVerifier - maxAge time.Duration - nonce func(ctx context.Context) string -} - -func (i *idTokenVerifier) Issuer() string { - return i.issuer -} - -func (i *idTokenVerifier) MaxAgeIAT() time.Duration { - return i.maxAgeIAT -} - -func (i *idTokenVerifier) Offset() time.Duration { - return i.offset -} - -func (i *idTokenVerifier) ClientID() string { - return i.clientID -} - -func (i *idTokenVerifier) SupportedSignAlgs() []string { - return i.supportedSignAlgs -} - -func (i *idTokenVerifier) KeySet() oidc.KeySet { - return i.keySet -} - -func (i *idTokenVerifier) Nonce(ctx context.Context) string { - return i.nonce(ctx) -} - -func (i *idTokenVerifier) ACR() oidc.ACRVerifier { - return i.acr -} - -func (i *idTokenVerifier) MaxAge() time.Duration { - return i.maxAge -} - -func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet) IDTokenVerifier { - return &idTokenVerifier{ - issuer: issuer, - clientID: clientID, - keySet: keySet, - offset: 5 * time.Second, - } -} - //VerifyTokens implement the Token Response Validation as defined in OIDC specification //https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation func VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (*oidc.IDTokenClaims, error) { @@ -167,3 +100,72 @@ func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAl } return nil } + +//NewIDTokenVerifier returns an implementation of `IDTokenVerifier` +//for `VerifyTokens` and `VerifyIDToken` +func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet) IDTokenVerifier { + return &idTokenVerifier{ + issuer: issuer, + clientID: clientID, + keySet: keySet, + offset: 5 * time.Second, + } +} + +type idTokenVerifier struct { + issuer string + maxAgeIAT time.Duration + offset time.Duration + clientID string + supportedSignAlgs []string + keySet oidc.KeySet + acr oidc.ACRVerifier + maxAge time.Duration + nonce func(ctx context.Context) string +} + +func (i *idTokenVerifier) Issuer() string { + return i.issuer +} + +func (i *idTokenVerifier) MaxAgeIAT() time.Duration { + return i.maxAgeIAT +} + +func (i *idTokenVerifier) Offset() time.Duration { + return i.offset +} + +func (i *idTokenVerifier) ClientID() string { + return i.clientID +} + +func (i *idTokenVerifier) SupportedSignAlgs() []string { + return i.supportedSignAlgs +} + +func (i *idTokenVerifier) KeySet() oidc.KeySet { + return i.keySet +} + +func (i *idTokenVerifier) Nonce(ctx context.Context) string { + return i.nonce(ctx) +} + +func (i *idTokenVerifier) ACR() oidc.ACRVerifier { + return i.acr +} + +func (i *idTokenVerifier) MaxAge() time.Duration { + return i.maxAge +} + +//deprecated: Use IDTokenVerifier (or oidc.Verifier) +type Verifier interface { + + //Verify checks the access_token and id_token and returns the `id token claims` + Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) + + //VerifyIDToken checks the id_token only and returns its `id token claims` + VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) +} diff --git a/pkg/utils/hash.go b/pkg/utils/hash.go index 78c007f..b7dfd9c 100644 --- a/pkg/utils/hash.go +++ b/pkg/utils/hash.go @@ -24,7 +24,8 @@ func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) { } func HashString(hash hash.Hash, s string, firstHalf bool) string { - hash.Write([]byte(s)) // hash documents that Write will never return an error + //nolint:errcheck + hash.Write([]byte(s)) size := hash.Size() if firstHalf { size = size / 2 diff --git a/pkg/utils/http.go b/pkg/utils/http.go index b3ed631..5da12af 100644 --- a/pkg/utils/http.go +++ b/pkg/utils/http.go @@ -25,17 +25,24 @@ type Encoder interface { Encode(src interface{}, dst map[string][]string) error } -func FormRequest(endpoint string, request interface{}) (*http.Request, error) { +func FormRequest(endpoint string, request interface{}, clientID, clientSecret string, header bool) (*http.Request, error) { form := make(map[string][]string) encoder := schema.NewEncoder() if err := encoder.Encode(request, form); err != nil { return nil, err } + if !header { + form["client_id"] = []string{clientID} + form["client_secret"] = []string{clientSecret} + } body := strings.NewReader(url.Values(form).Encode()) req, err := http.NewRequest("POST", endpoint, body) if err != nil { return nil, err } + if header { + req.SetBasicAuth(clientID, clientSecret) + } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") return req, nil }