diff --git a/pkg/oidc/keyset.go b/pkg/oidc/keyset.go index f9bed2f..abe55d1 100644 --- a/pkg/oidc/keyset.go +++ b/pkg/oidc/keyset.go @@ -20,3 +20,13 @@ type KeySet interface { // use any HTTP client associated with the context through ClientContext. VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) } + +func CheckKey(keyID string, jws *jose.JSONWebSignature, keys ...jose.JSONWebKey) ([]byte, error, bool) { + for _, key := range keys { + if keyID == "" || key.KeyID == keyID { + payload, err := jws.Verify(&key) + return payload, err, true + } + } + return nil, nil, false +} diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 3ec7631..7611090 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -7,7 +7,7 @@ import ( "github.com/caos/oidc/pkg/utils" ) -func DiscoveryHandler(c Configuration, s Signer) func(http.ResponseWriter, *http.Request) { +func discoveryHandler(c Configuration, s Signer) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { Discover(w, CreateDiscoveryConfig(c, s)) } diff --git a/pkg/op/op.go b/pkg/op/op.go index 0bad929..1d9a750 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -13,7 +13,6 @@ import ( "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) @@ -51,6 +50,7 @@ type OpenIDProvider interface { Decoder() utils.Decoder Encoder() utils.Encoder IDTokenHintVerifier() IDTokenHintVerifier + JWTProfileVerifier() JWTProfileVerifier Crypto() Crypto DefaultLogoutRedirectURI() string Signer() Signer @@ -72,9 +72,9 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router handlers.AllowedHeaders([]string{"authorization", "content-type"}), handlers.AllowedOriginValidator(allowAllOrigins), )) - router.HandleFunc(healthzEndpoint, Healthz) - router.HandleFunc(readinessEndpoint, Ready(o.Probes())) - router.HandleFunc(oidc.DiscoveryEndpoint, DiscoveryHandler(o, o.Signer())) + router.HandleFunc(healthzEndpoint, healthzHandler) + router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) + router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer())) router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) router.Handle(o.AuthorizationEndpoint().Relative()+"/{id}", intercept(authorizeCallbackHandler(o))) router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o))) @@ -131,8 +131,6 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO o.signer = NewDefaultSigner(ctx, storage, keyCh) go EnsureKey(ctx, storage, keyCh, o.timer, o.retry) - o.idTokenHintVerifier = NewIDTokenHintVerifier(config.Issuer, o) - o.httpHandler = CreateRouter(o, o.interceptors...) o.decoder = schema.NewDecoder() @@ -151,6 +149,7 @@ type openidProvider struct { storage Storage signer Signer idTokenHintVerifier IDTokenHintVerifier + jwtProfileVerifier JWTProfileVerifier crypto Crypto httpHandler http.Handler decoder *schema.Decoder @@ -205,9 +204,19 @@ func (o *openidProvider) Encoder() utils.Encoder { } func (o *openidProvider) IDTokenHintVerifier() IDTokenHintVerifier { + if o.idTokenHintVerifier == nil { + o.idTokenHintVerifier = NewIDTokenHintVerifier(o.Issuer(), &openIDKeySet{o.Storage()}) + } return o.idTokenHintVerifier } +func (o *openidProvider) JWTProfileVerifier() JWTProfileVerifier { + if o.jwtProfileVerifier == nil { + o.jwtProfileVerifier = NewJWTProfileVerifier(o.Storage(), o.Issuer()) + } + return o.jwtProfileVerifier +} + func (o *openidProvider) Crypto() Crypto { return o.crypto } @@ -231,19 +240,23 @@ func (o *openidProvider) HttpHandler() http.Handler { return o.httpHandler } +type openIDKeySet struct { + Storage +} + //VerifySignature implements the oidc.KeySet interface //providing an implementation for the keys stored in the OP Storage interface -func (o *openidProvider) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { +func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { keyID := "" for _, sig := range jws.Signatures { keyID = sig.Header.KeyID break } - keySet, err := o.Storage().GetKeySet(ctx) + keySet, err := o.Storage.GetKeySet(ctx) if err != nil { return nil, errors.New("error fetching keys") } - payload, err, ok := rp.CheckKey(keyID, keySet.Keys, jws) + payload, err, ok := oidc.CheckKey(keyID, jws, keySet.Keys...) if !ok { return nil, errors.New("invalid kid") } diff --git a/pkg/op/probes.go b/pkg/op/probes.go index ab12851..7dc00a9 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -10,11 +10,11 @@ import ( type ProbesFn func(context.Context) error -func Healthz(w http.ResponseWriter, r *http.Request) { +func healthzHandler(w http.ResponseWriter, r *http.Request) { ok(w) } -func Ready(probes []ProbesFn) func(w http.ResponseWriter, r *http.Request) { +func readyHandler(probes []ProbesFn) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { Readiness(w, r, probes...) } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 449c2dd..669b08e 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -30,7 +30,7 @@ type OPStorage interface { AuthorizeClientIDSecret(context.Context, string, string) error GetUserinfoFromScopes(context.Context, string, []string) (*oidc.Userinfo, error) GetUserinfoFromToken(context.Context, string, string) (*oidc.Userinfo, error) - GetKeyByID(ctx context.Context, keyID string) (*jose.JSONWebKeySet, error) + GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) } type Storage interface { diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index 72a688c..18e7a52 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -4,12 +4,8 @@ import ( "context" "errors" "net/http" - "time" - - "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) @@ -20,11 +16,7 @@ type Exchanger interface { Signer() Signer Crypto() Crypto AuthMethodPostSupported() bool -} - -type VerifyExchanger interface { - Exchanger - ClientJWTVerifier() oidc.Verifier + JWTProfileVerifier() JWTProfileVerifier } func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) { @@ -34,16 +26,16 @@ func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Reque CodeExchange(w, r, exchanger) return case string(oidc.GrantTypeBearer): - ex, _ := exchanger.(VerifyExchanger) - JWTExchange(w, r, ex) + JWTProfile(w, r, exchanger) return - case "excahnge": + case "exchange": TokenExchange(w, r, exchanger) case "": RequestError(w, r, ErrInvalidRequest("grant_type missing")) return default: - + RequestError(w, r, ErrInvalidRequest("grant_type not supported")) + return } } } @@ -144,41 +136,13 @@ func AuthorizeCodeChallenge(ctx context.Context, tokenReq *oidc.AccessTokenReque return authReq, nil } -type ClientJWTVerifier struct { - claims *oidc.JWTTokenRequest - storage Storage - issuer string -} - -func (c ClientJWTVerifier) Storage() Storage { - return c.storage -} - -func (c ClientJWTVerifier) Issuer() string { - return c.claims.Issuer -} - -func (c ClientJWTVerifier) ClientID() string { - return c.issuer -} - -func (c ClientJWTVerifier) MaxAgeIAT() time.Duration { - //TODO: define in conf/opts - return 1 * time.Hour -} - -func (c ClientJWTVerifier) Offset() time.Duration { - //TODO: define in conf/opts - return time.Second -} - -func JWTExchange(w http.ResponseWriter, r *http.Request, exchanger VerifyExchanger) { - assertion, err := ParseJWTTokenRequest(r, exchanger.Decoder()) +func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { + assertion, err := ParseJWTProfileRequest(r, exchanger.Decoder()) if err != nil { RequestError(w, r, err) } - claims, err := VerifyJWTAssertion(r.Context(), assertion, exchanger) + claims, err := VerifyJWTAssertion(r.Context(), assertion, exchanger.JWTProfileVerifier()) if err != nil { RequestError(w, r, err) return @@ -192,70 +156,7 @@ func JWTExchange(w http.ResponseWriter, r *http.Request, exchanger VerifyExchang utils.MarshalJSON(w, resp) } -type JWTAssertionVerifier interface { - Storage() Storage - oidc.Verifier -} - -func VerifyJWTAssertion(ctx context.Context, assertion string, exchanger Exchanger) (*oidc.JWTTokenRequest, error) { - verifier := &ClientJWTVerifier{ - storage: exchanger.Storage(), - issuer: exchanger.Issuer(), - claims: new(oidc.JWTTokenRequest), - } - payload, err := oidc.ParseToken(assertion, verifier.claims) - if err != nil { - return nil, err - } - - if err = oidc.CheckAudience(verifier.claims, verifier.issuer); err != nil { - return nil, err - } - - if err = oidc.CheckExpiration(verifier.claims, verifier.Offset()); err != nil { - return nil, err - } - - if err = oidc.CheckIssuedAt(verifier.claims, verifier.MaxAgeIAT(), verifier.Offset()); err != nil { - return nil, err - } - - if verifier.claims.Issuer != verifier.claims.Subject { - //TODO: implement delegation (openid core / oauth rfc) - } - verifier.Storage().GetClientByClientID(ctx, verifier.claims.Subject) - - keySet := &ClientAssertionKeySet{exchanger.Storage(), verifier.claims.Subject} - - if err = oidc.CheckSignature(ctx, assertion, payload, verifier.claims, nil, keySet); err != nil { - return nil, err - } - return verifier.claims, nil -} - -type ClientAssertionKeySet struct { - Storage - id string -} - -func (c *ClientAssertionKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { - keyID := "" - for _, sig := range jws.Signatures { - keyID = sig.Header.KeyID - break - } - keySet, err := c.Storage.GetKeyByID(ctx, keyID) - if err != nil { - return nil, errors.New("error fetching keys") - } - payload, err, ok := rp.CheckKey(keyID, keySet.Keys, jws) - if !ok { - return nil, errors.New("invalid kid") - } - return payload, err -} - -func ParseJWTTokenRequest(r *http.Request, decoder utils.Decoder) (string, error) { +func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (string, error) { err := r.ParseForm() if err != nil { return "", ErrInvalidRequest("error parsing form") @@ -267,7 +168,6 @@ func ParseJWTTokenRequest(r *http.Request, decoder utils.Decoder) (string, error if err != nil { return "", ErrInvalidRequest("error decoding form") } - //TODO: validations return tokenReq.Token, nil } diff --git a/pkg/op/id_token_hint_verifier.go b/pkg/op/verifier_id_token_hint.go similarity index 100% rename from pkg/op/id_token_hint_verifier.go rename to pkg/op/verifier_id_token_hint.go diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go new file mode 100644 index 0000000..807700f --- /dev/null +++ b/pkg/op/verifier_jwt_profile.go @@ -0,0 +1,99 @@ +package op + +import ( + "context" + "errors" + "time" + + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/oidc" +) + +type JWTProfileVerifier interface { + oidc.Verifier + Storage() Storage +} + +type jwtProfileVerifier struct { + storage Storage + issuer string +} + +func NewJWTProfileVerifier(storage Storage, issuer string) JWTProfileVerifier { + return &jwtProfileVerifier{ + storage: storage, + issuer: issuer, + } +} + +func (v *jwtProfileVerifier) Issuer() string { + return v.issuer +} + +func (v *jwtProfileVerifier) Storage() Storage { + return v.storage +} + +func (v *jwtProfileVerifier) MaxAgeIAT() time.Duration { + //TODO: define in conf/opts + return 1 * time.Hour +} + +func (v *jwtProfileVerifier) Offset() time.Duration { + //TODO: define in conf/opts + return time.Second +} + +func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { + request := new(oidc.JWTTokenRequest) + payload, err := oidc.ParseToken(assertion, request) + if err != nil { + return nil, err + } + + if err = oidc.CheckAudience(request, v.Issuer()); err != nil { + return nil, err + } + + if err = oidc.CheckExpiration(request, v.Offset()); err != nil { + return nil, err + } + + if err = oidc.CheckIssuedAt(request, v.MaxAgeIAT(), v.Offset()); err != nil { + return nil, err + } + + if request.Issuer != request.Subject { + //TODO: implement delegation (openid core / oauth rfc) + } + + keySet := &jwtProfileKeySet{v.Storage(), request.Subject} + + if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil { + return nil, err + } + return request, nil +} + +type jwtProfileKeySet struct { + Storage + userID string +} + +func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { + keyID := "" + for _, sig := range jws.Signatures { + keyID = sig.Header.KeyID + break + } + key, err := k.Storage.GetKeyByIDAndUserID(ctx, keyID, k.userID) + if err != nil { + return nil, errors.New("error fetching keys") + } + payload, err, ok := oidc.CheckKey(keyID, jws, *key) + if !ok { + return nil, errors.New("invalid kid") + } + return payload, err +} diff --git a/pkg/rp/default_rp.go b/pkg/rp/default_rp.go deleted file mode 100644 index fdf0b38..0000000 --- a/pkg/rp/default_rp.go +++ /dev/null @@ -1,236 +0,0 @@ -package rp - -import ( - "context" - "net/http" - "strings" - - "golang.org/x/oauth2" - - "github.com/caos/oidc/pkg/oidc" - grants_tx "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" - "github.com/caos/oidc/pkg/utils" -) - -const ( - idTokenKey = "id_token" - stateParam = "state" - pkceCode = "pkce" -) - -//deprecated: use NewRelayingParty instead -//DefaultRP implements the `DelegationTokenExchangeRP` interface extending the `RelayingParty` interface -type DefaultRP struct { - endpoints Endpoints - - oauthConfig oauth2.Config - config *Config - pkce bool - - httpClient *http.Client - cookieHandler *utils.CookieHandler - - errorHandler func(http.ResponseWriter, *http.Request, string, string, string) - - idTokenVerifier IDTokenVerifier - verifierOpts []ConfFunc - 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 -//if no verifier is provided using the options the `DefaultVerifier` is set -func NewDefaultRP(rpConfig *Config, rpOpts ...DefaultRPOpts) (DelegationTokenExchangeRP, error) { - foundOpenID := false - for _, scope := range rpConfig.Scopes { - if scope == "openid" { - foundOpenID = true - } - } - - p := &DefaultRP{ - config: rpConfig, - httpClient: utils.DefaultHTTPClient, - onlyOAuth2: !foundOpenID, - } - - for _, optFunc := range rpOpts { - optFunc(p) - } - - if rpConfig.Endpoints.TokenURL != "" && rpConfig.Endpoints.AuthURL != "" { - p.oauthConfig = p.getOAuthConfig(rpConfig.Endpoints) - } else { - if err := p.discover(); err != nil { - return nil, err - } - } - - if p.errorHandler == nil { - p.errorHandler = DefaultErrorHandler - } - - if p.idTokenVerifier == nil { - p.idTokenVerifier = NewIDTokenVerifier(rpConfig.Issuer, rpConfig.ClientID, NewRemoteKeySet(p.httpClient, p.endpoints.JKWsURL)) - } - - return p, nil -} - -//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) { - p.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) DefaultRPOpts { - return func(p *DefaultRP) { - p.pkce = true - p.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) DefaultRPOpts { - return func(p *DefaultRP) { - p.httpClient = client - } -} - -func WithVerifierOpts(opts ...ConfFunc) DefaultRPOpts { - return func(p *DefaultRP) { - 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 { - 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 AuthURLHandler( - func() string { - return 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 parsed together with the oauth2 tokens (access, refresh) -func (p *DefaultRP) CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { - 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 CodeExchangeHandler(callback, p) -} - -// func (p *DefaultRP) Introspect(ctx context.Context, accessToken string) (oidc.TokenIntrospectResponse, error) { -// // req := &http.Request{} -// // resp, err := p.httpClient.Do(req) -// // if err != nil { - -// // } -// // p.endpoints.IntrospectURL -// return nil, nil -// } - -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 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 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 TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), p) -} - -func (p *DefaultRP) discover() error { - wellKnown := strings.TrimSuffix(p.config.Issuer, "/") + oidc.DiscoveryEndpoint - req, err := http.NewRequest("GET", wellKnown, nil) - if err != nil { - return err - } - discoveryConfig := new(oidc.DiscoveryConfiguration) - err = utils.HttpRequest(p.httpClient, req, &discoveryConfig) - if err != nil { - return err - } - p.endpoints = GetEndpoints(discoveryConfig) - p.oauthConfig = p.getOAuthConfig(p.endpoints.Endpoint) - return nil -} - -func (p *DefaultRP) getOAuthConfig(endpoint oauth2.Endpoint) oauth2.Config { - return oauth2.Config{ - ClientID: p.config.ClientID, - ClientSecret: p.config.ClientSecret, - Endpoint: endpoint, - RedirectURL: p.config.CallbackURL, - Scopes: p.config.Scopes, - } -} - -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 deleted file mode 100644 index d8895ce..0000000 --- a/pkg/rp/default_verifier.go +++ /dev/null @@ -1,190 +0,0 @@ -package rp - -import ( - "context" - "time" - - "github.com/caos/oidc/pkg/oidc" -) - -//deprecated: use IDTokenVerifier or oidc.Verifier interfaces -//DefaultVerifier implements the `Verifier` interface -type DefaultVerifier struct { - config *verifierConfig - keySet oidc.KeySet -} - -//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 { - conf := &verifierConfig{ - issuer: issuer, - clientID: clientID, - iat: &iatConfig{ - // offset: time.Duration(500 * time.Millisecond), - }, - } - - for _, opt := range confOpts { - if opt != nil { - opt(conf) - } - } - return &DefaultVerifier{config: conf, keySet: keySet} -} - -//WithIgnoreAudience will turn off validation for audience claim (should only be used for id_token_hints) -func WithIgnoreAudience() func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.ignoreAudience = true - } -} - -//WithIgnoreExpiration will turn off validation for expiration claim (should only be used for id_token_hints) -func WithIgnoreExpiration() func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.ignoreExpiration = true - } -} - -//WithIgnoreIssuedAt will turn off iat claim verification -func WithIgnoreIssuedAt() func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.iat.ignore = true - } -} - -//WithIssuedAtOffset mitigates the risk of iat to be in the future -//because of clock skews with the ability to add an offset to the current time -func WithIssuedAtOffset(offset time.Duration) func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.iat.offset = offset - } -} - -//WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now -func WithIssuedAtMaxAge(maxAge time.Duration) func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.iat.maxAge = maxAge - } -} - -//WithNonce TODO: ? -func WithNonce(nonce string) func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.nonce = nonce - } -} - -//WithACRVerifier sets the verifier for the acr claim -func WithACRVerifier(verifier oidc.ACRVerifier) func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.acr = verifier - } -} - -//WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now -func WithAuthTimeMaxAge(maxAge time.Duration) func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.maxAge = maxAge - } -} - -//WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm -func WithSupportedSigningAlgorithms(algs ...string) func(*verifierConfig) { - return func(conf *verifierConfig) { - conf.supportedSignAlgs = algs - } -} - -type verifierConfig struct { - issuer string - clientID string - nonce string - ignoreAudience bool - ignoreExpiration bool - iat *iatConfig - acr oidc.ACRVerifier - maxAge time.Duration - supportedSignAlgs []string - - // httpClient *http.Client - - now time.Time -} - -type iatConfig struct { - ignore bool - offset time.Duration - 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 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 -func (v *DefaultVerifier) Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) { - v.config.now = time.Now().UTC() - 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) { - return VerifyIDToken(ctx, idTokenString, v) -} - -func (v *DefaultVerifier) now() time.Time { - if v.config.now.IsZero() { - v.config.now = time.Now().UTC().Round(time.Second) - } - return v.config.now -} - -func (v *DefaultVerifier) Issuer() string { - return v.config.issuer -} - -func (v *DefaultVerifier) ClientID() string { - return v.config.clientID -} - -func (v *DefaultVerifier) SupportedSignAlgs() []string { - return v.config.supportedSignAlgs -} - -func (v *DefaultVerifier) KeySet() oidc.KeySet { - return v.keySet -} - -func (v *DefaultVerifier) ACR() oidc.ACRVerifier { - return v.config.acr -} - -func (v *DefaultVerifier) MaxAge() time.Duration { - return v.config.maxAge -} - -func (v *DefaultVerifier) MaxAgeIAT() time.Duration { - return v.config.iat.maxAge -} - -func (v *DefaultVerifier) Offset() time.Duration { - return v.config.iat.offset -} - -func (v *DefaultVerifier) Nonce(ctx context.Context) string { - return "" -} diff --git a/pkg/rp/jwks.go b/pkg/rp/jwks.go index 97b1e6f..339fc93 100644 --- a/pkg/rp/jwks.go +++ b/pkg/rp/jwks.go @@ -74,7 +74,7 @@ func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig } keys := r.keysFromCache() - payload, err, ok := CheckKey(keyID, keys, jws) + payload, err, ok := oidc.CheckKey(keyID, jws, keys...) if ok { return payload, err } @@ -84,7 +84,7 @@ func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig return nil, fmt.Errorf("fetching keys %v", err) } - payload, err, ok = CheckKey(keyID, keys, jws) + payload, err, ok = oidc.CheckKey(keyID, jws, keys...) if !ok { return nil, errors.New("invalid kid") } diff --git a/pkg/rp/jws.go b/pkg/rp/jws.go deleted file mode 100644 index 20ab896..0000000 --- a/pkg/rp/jws.go +++ /dev/null @@ -1,15 +0,0 @@ -package rp - -import ( - "gopkg.in/square/go-jose.v2" -) - -func CheckKey(keyID string, keys []jose.JSONWebKey, jws *jose.JSONWebSignature) ([]byte, error, bool) { - for _, key := range keys { - if keyID == "" || key.KeyID == keyID { - payload, err := jws.Verify(&key) - return payload, err, true - } - } - return nil, nil, false -} diff --git a/pkg/rp/relaying_party.go b/pkg/rp/relaying_party.go index bab2512..43d0c97 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/rp/relaying_party.go @@ -13,6 +13,12 @@ import ( "golang.org/x/oauth2" ) +const ( + idTokenKey = "id_token" + stateParam = "state" + pkceCode = "pkce" +) + //RelayingParty declares the minimal interface for oidc clients type RelayingParty interface { //OAuthConfig returns the oauth2 Config @@ -27,54 +33,12 @@ type RelayingParty interface { //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 - - //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) - - //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) - - //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() - */ 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 -// -// //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) @@ -84,9 +48,8 @@ var ( type relayingParty struct { endpoints Endpoints - oauthConfig *oauth2.Config - config *Configuration - pkce bool + config *Configuration + pkce bool httpClient *http.Client cookieHandler *utils.CookieHandler @@ -94,12 +57,12 @@ type relayingParty struct { errorHandler func(http.ResponseWriter, *http.Request, string, string, string) idTokenVerifier IDTokenVerifier - verifierOpts []ConfFunc + verifierOpts []VerifierOption oauth2Only bool } func (rp *relayingParty) OAuthConfig() *oauth2.Config { - return rp.oauthConfig + return rp.config.Config } func (rp *relayingParty) IsPKCE() bool { @@ -119,11 +82,14 @@ func (rp *relayingParty) IsOAuth2Only() bool { } func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier { + if rp.idTokenVerifier == nil { + rp.idTokenVerifier = NewIDTokenVerifier(rp.config.Issuer, rp.config.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) + } return rp.idTokenVerifier } func (rp *relayingParty) Client(ctx context.Context, token *oauth2.Token) *http.Client { - panic("implement me") + return rp.config.Config.Client(ctx, token) } func (rp *relayingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { @@ -147,15 +113,12 @@ func NewRelayingParty(config *Configuration, options ...Option) (RelayingParty, optFunc(rp) } - rp.oauthConfig = config.Config - if config.Endpoint.AuthURL != "" && config.Endpoint.TokenURL != "" { - rp.oauthConfig = config.Config - } else { + if isOpenID && config.Endpoint.AuthURL == "" && config.Endpoint.TokenURL == "" { endpoints, err := Discover(config.Issuer, rp.httpClient) if err != nil { return nil, err } - rp.oauthConfig.Endpoint = endpoints.Endpoint + rp.config.Endpoint = endpoints.Endpoint rp.endpoints = endpoints } @@ -170,6 +133,47 @@ func NewRelayingParty(config *Configuration, options ...Option) (RelayingParty, return rp, nil } +func NewRelayingParty2(clientID, clientSecret, redirectURI string, options ...Option) (RelayingParty, error) { + rp := &relayingParty{ + config: &Configuration{ + Config: &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURL: redirectURI, + }, + }, + httpClient: utils.DefaultHTTPClient, + oauth2Only: true, + } + + for _, optFunc := range options { + optFunc(rp) + } + + if !rp.oauth2Only && rp.config.Endpoint.AuthURL == "" && rp.config.Endpoint.TokenURL == "" { + endpoints, err := Discover(rp.config.Issuer, rp.httpClient) + if err != nil { + return nil, err + } + rp.config.Endpoint = endpoints.Endpoint + rp.endpoints = endpoints + } + + if rp.errorHandler == nil { + rp.errorHandler = DefaultErrorHandler + } + + return rp, nil +} + +func WithOIDC(issuer string, scopes []string) Option { + return func(rp *relayingParty) { + rp.config.Issuer = issuer + rp.config.Scopes = scopes + rp.oauth2Only = false + } +} + //DefaultRPOpts is the type for providing dynamic options to the DefaultRP type Option func(*relayingParty) @@ -197,7 +201,7 @@ func WithHTTPClient(client *http.Client) Option { } } -func WithVerifierOpts(opts ...ConfFunc) Option { +func WithVerifierOpts(opts ...VerifierOption) Option { return func(rp *relayingParty) { rp.verifierOpts = opts } @@ -419,13 +423,3 @@ func isOpenID(scopes []string) bool { } 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/verifier.go b/pkg/rp/verifier.go index d079b53..ef2cf87 100644 --- a/pkg/rp/verifier.go +++ b/pkg/rp/verifier.go @@ -103,16 +103,68 @@ func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAl //NewIDTokenVerifier returns an implementation of `IDTokenVerifier` //for `VerifyTokens` and `VerifyIDToken` -func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet) IDTokenVerifier { - return &idTokenVerifier{ +func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) IDTokenVerifier { + v := &idTokenVerifier{ issuer: issuer, clientID: clientID, keySet: keySet, - offset: 5 * time.Second, + offset: 1 * time.Second, nonce: func(_ context.Context) string { return "" }, } + + for _, opts := range options { + opts(v) + } + + return v +} + +//VerifierOption is the type for providing dynamic options to the IDTokenVerifier +type VerifierOption func(*idTokenVerifier) + +//WithIssuedAtOffset mitigates the risk of iat to be in the future +//because of clock skews with the ability to add an offset to the current time +func WithIssuedAtOffset(offset time.Duration) func(*idTokenVerifier) { + return func(v *idTokenVerifier) { + v.offset = offset + } +} + +//WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now +func WithIssuedAtMaxAge(maxAge time.Duration) func(*idTokenVerifier) { + return func(v *idTokenVerifier) { + v.maxAge = maxAge + } +} + +//WithNonce sets the function to check the nonce +func WithNonce(nonce func(context.Context) string) VerifierOption { + return func(v *idTokenVerifier) { + v.nonce = nonce + } +} + +//WithACRVerifier sets the verifier for the acr claim +func WithACRVerifier(verifier oidc.ACRVerifier) VerifierOption { + return func(v *idTokenVerifier) { + v.acr = verifier + } +} + +//WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now +func WithAuthTimeMaxAge(maxAge time.Duration) VerifierOption { + return func(v *idTokenVerifier) { + v.maxAge = maxAge + } +} + +//WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm +func WithSupportedSigningAlgorithms(algs ...string) VerifierOption { + return func(v *idTokenVerifier) { + v.supportedSignAlgs = algs + } } type idTokenVerifier struct {