From 1000a04b91e033764e0370ce5e7d6ff70fcc753f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Tue, 10 Dec 2019 14:32:08 +0100 Subject: [PATCH] simplify oidc middleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- go.mod | 1 - go.sum | 3 - middleware/openidconnect.go | 146 ++++-------------------------------- oidc/claims.go | 55 -------------- oidc/option.go | 36 --------- 5 files changed, 16 insertions(+), 225 deletions(-) diff --git a/go.mod b/go.mod index 20d6a7faa..694975658 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/micro/go-plugins v1.5.1 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/prometheus/client_golang v1.2.1 - github.com/restic/calens v0.1.0 // indirect github.com/rs/zerolog v1.17.2 github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce go.opencensus.io v0.22.2 diff --git a/go.sum b/go.sum index 8c25af109..55a88c536 100644 --- a/go.sum +++ b/go.sum @@ -219,7 +219,6 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stomp/stomp v2.0.3+incompatible/go.mod h1:VqCtqNZv1226A1/79yh+rMiFUcfY3R109np+7ke4n0c= github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= -github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -541,8 +540,6 @@ github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQl github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/restic/calens v0.1.0 h1:RHGokdZ72dICyIz1EjEsfZwUhvNZz/zy2SawxJktdWA= -github.com/restic/calens v0.1.0/go.mod h1:u67f5msOjCTDYNzOf/NoAUSdmXP03YXPCwIQLYADy5M= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= diff --git a/middleware/openidconnect.go b/middleware/openidconnect.go index c202de19c..7e5b92f93 100644 --- a/middleware/openidconnect.go +++ b/middleware/openidconnect.go @@ -3,9 +3,7 @@ package middleware import ( "context" "crypto/tls" - "encoding/json" "fmt" - "io/ioutil" "net/http" "strings" "time" @@ -39,7 +37,6 @@ func OpenIDConnect(opts ...ocisoidc.Option) func(http.Handler) http.Handler { } var oidcProvider *oidc.Provider - var oidcMetadata *ocisoidc.ProviderMetadata return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -77,140 +74,29 @@ func OpenIDConnect(opts ...ocisoidc.Option) func(http.Handler) http.Handler { return } oidcProvider = provider - metadata := &ocisoidc.ProviderMetadata{} - if err := provider.Claims(metadata); err != nil { - opt.Logger.Error().Err(err).Msg("could not not unmarshal provider metadata") - w.WriteHeader(http.StatusInternalServerError) - return - } - oidcMetadata = metadata } - provider := oidcProvider // The claims we want to have var claims ocisoidc.StandardClaims - if oidcMetadata.IntrospectionEndpoint == "" { - - opt.Logger.Debug().Msg("no introspection endpoint, trying to decode access token as jwt") - //maybe our access token is a jwt token - c := &oidc.Config{ - ClientID: opt.Audience, - SupportedSigningAlgs: opt.SigningAlgs, - } - if opt.SkipChecks { // not safe but only way for simplesamlphp to work with an almost compliant oidc (for now) - c.SkipClientIDCheck = true - c.SkipIssuerCheck = true - } - verifier := provider.Verifier(c) - idToken, err := verifier.Verify(customCtx, token) - if err != nil { - opt.Logger.Error().Err(err).Str("token", token).Msg("could not verify jwt") - w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Bearer realm="%s"`, opt.Realm)) - http.Error(w, ErrInvalidToken.Error(), http.StatusUnauthorized) - return - } - if err := idToken.Claims(&claims); err != nil { - opt.Logger.Error().Err(err).Str("token", token).Interface("id_token", idToken).Msg("failed to parse claims") - w.WriteHeader(http.StatusInternalServerError) - return - } - - } else { - - // we need to lookup the id token with the access token we got - // see oidc IDToken.Verifytoken - - data := fmt.Sprintf("token=%s&token_type_hint=access_token", token) - req, err := http.NewRequest("POST", oidcMetadata.IntrospectionEndpoint, strings.NewReader(data)) - if err != nil { - opt.Logger.Error().Err(err).Msg("could not create introspection request") - w.WriteHeader(http.StatusInternalServerError) - return - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - // we follow https://tools.ietf.org/html/rfc7662 - req.Header.Set("Accept", "application/json") - if opt.ClientID != "" { - req.SetBasicAuth(opt.ClientID, opt.ClientSecret) - } - - res, err := customHTTPClient.Do(req) - if err != nil { - opt.Logger.Error().Err(err).Str("token", token).Msg("could not introspect access token") - w.WriteHeader(http.StatusInternalServerError) - return - } - defer res.Body.Close() - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - opt.Logger.Error().Err(err).Msg("could not read introspection response body") - w.WriteHeader(http.StatusInternalServerError) - return - } - - opt.Logger.Debug().Str("body", string(body)).Msg("body") - switch strings.Split(res.Header.Get("Content-Type"), ";")[0] { - // application/jwt is in draft https://tools.ietf.org/html/draft-ietf-oauth-jwt-introspection-response-03 - case "application/jwt": - // verify the jwt - // TODO this is a yet untested verification of jwt encoded introspection response - - verifier := provider.Verifier(&oidc.Config{ClientID: opt.Audience}) - idToken, err := verifier.Verify(customCtx, string(body)) - if err != nil { - opt.Logger.Error().Err(err).Str("token", string(body)).Msg("could not verify jwt") - w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Bearer realm="%s"`, opt.Realm)) - http.Error(w, ErrInvalidToken.Error(), http.StatusUnauthorized) - return - } - - if err := idToken.Claims(&claims); err != nil { - opt.Logger.Error().Err(err).Str("token", string(body)).Interface("id_token", idToken).Msg("failed to parse claims") - w.WriteHeader(http.StatusInternalServerError) - return - } - case "application/json": - var ir ocisoidc.IntrospectionResponse - // parse json - if err := json.Unmarshal(body, &ir); err != nil { - opt.Logger.Error().Err(err).Str("token", string(body)).Msg("failed to parse introspection response") - w.WriteHeader(http.StatusInternalServerError) - return - } - // verify the auth token is still active - if !ir.Active { - opt.Logger.Error().Interface("ir", ir).Str("body", string(body)).Msg("token no longer active") - w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Bearer realm="%s"`, opt.Realm)) - http.Error(w, ErrInvalidToken.Error(), http.StatusUnauthorized) - return - } - // resolve user info here? cache it? - oauth2Token := &oauth2.Token{ - AccessToken: token, - } - userInfo, err := provider.UserInfo(customCtx, oauth2.StaticTokenSource(oauth2Token)) - if err != nil { - opt.Logger.Error().Err(err).Str("token", string(body)).Msg("Failed to get userinfo") - w.WriteHeader(http.StatusInternalServerError) - return - } - if err := userInfo.Claims(&claims); err != nil { - opt.Logger.Error().Err(err).Interface("userinfo", userInfo).Msg("failed to unmarshal userinfo claims") - w.WriteHeader(http.StatusInternalServerError) - return - } - claims.Iss = ir.Iss - opt.Logger.Debug().Interface("claims", claims).Interface("userInfo", userInfo).Msg("unmarshalled userinfo") - - default: - opt.Logger.Error().Str("content-type", res.Header.Get("Content-Type")).Msg("unknown content type") - w.WriteHeader(http.StatusInternalServerError) - return - } + // TODO cache userinfo for access token if we can determine the expiry (which works in case it is a jwt based access token) + oauth2Token := &oauth2.Token{ + AccessToken: token, + } + userInfo, err := oidcProvider.UserInfo(customCtx, oauth2.StaticTokenSource(oauth2Token)) + if err != nil { + opt.Logger.Error().Err(err).Str("token", string(token)).Msg("Failed to get userinfo") + http.Error(w, ErrInvalidToken.Error(), http.StatusUnauthorized) + return } + // parse claims + if err := userInfo.Claims(&claims); err != nil { + opt.Logger.Error().Err(err).Interface("userinfo", userInfo).Msg("failed to unmarshal userinfo claims") + w.WriteHeader(http.StatusInternalServerError) + return + } + opt.Logger.Debug().Interface("claims", claims).Interface("userInfo", userInfo).Msg("unmarshalled userinfo") // store claims in context // uses the original context, not the one with probably reduced security nr := r.WithContext(ocisoidc.NewContext(r.Context(), &claims)) diff --git a/oidc/claims.go b/oidc/claims.go index fe401cb87..3292b6259 100644 --- a/oidc/claims.go +++ b/oidc/claims.go @@ -171,58 +171,3 @@ type StandardClaims struct { Address map[string]interface{} `json:"address,omitempty"` KCIdentity map[string]string `json:"kc.identity,omitempty"` } - -// The IntrospectionResponse is a JSON object [RFC7159] in -// "application/json" format with the following top-level members. -// see https://tools.ietf.org/html/rfc7662#section-2.2 -type IntrospectionResponse struct { - // REQUIRED. Boolean indicator of whether or not the presented token - // is currently active. The specifics of a token's "active" state - // will vary depending on the implementation of the authorization - // server and the information it keeps about its tokens, but a "true" - // value return for the "active" property will generally indicate - // that a given token has been issued by this authorization server, - // has not been revoked by the resource owner, and is within its - // given time window of validity (e.g., after its issuance time and - // before its expiration time). See Section 4 for information on - // implementation of such checks. - Active bool `json:"active"` - // OPTIONAL. A JSON string containing a space-separated list of - // scopes associated with this token, in the format described in - // Section 3.3 of OAuth 2.0 [RFC6749]. - Scope string `json:"scope,omitempty"` - // OPTIONAL. Client identifier for the OAuth 2.0 client that - // requested this token. - ClientID string `json:"client_id,omitempty"` - // OPTIONAL. Human-readable identifier for the resource owner who - // authorized this token. - Username string `json:"username,omitempty"` - // OPTIONAL. Type of the token as defined in Section 5.1 of OAuth - // 2.0 [RFC6749]. - TokenType string `json:"token_type,omitempty"` - // OPTIONAL. Integer timestamp, measured in the number of seconds - // since January 1 1970 UTC, indicating when this token will expire, - // as defined in JWT [RFC7519]. - Exp int64 `json:"exp,omitempty"` - // OPTIONAL. Integer timestamp, measured in the number of seconds - // since January 1 1970 UTC, indicating when this token was - // originally issued, as defined in JWT [RFC7519]. - Iat int64 `json:"iat,omitempty"` - // OPTIONAL. Integer timestamp, measured in the number of seconds - // since January 1 1970 UTC, indicating when this token is not to be - // used before, as defined in JWT [RFC7519]. - Nbf int64 `json:"nbf,omitempty"` - // OPTIONAL. Subject of the token, as defined in JWT [RFC7519]. - // Usually a machine-readable identifier of the resource owner who - // authorized this token. - Sub string `json:"sub,omitempty"` - // OPTIONAL. Service-specific string identifier or list of string - // identifiers representing the intended audience for this token, as - // defined in JWT [RFC7519]. - Aud string `json:"aud,omitempty"` - // OPTIONAL. String representing the issuer of this token, as - // defined in JWT [RFC7519]. - Iss string `json:"iss,omitempty"` - // OPTIONAL. String identifier for the token, as defined in JWT [RFC7519]. - Jti string `json:"jti,omitempty"` -} diff --git a/oidc/option.go b/oidc/option.go index 370f93194..b725ca8c0 100644 --- a/oidc/option.go +++ b/oidc/option.go @@ -15,18 +15,10 @@ type Options struct { Endpoint string // Realm to use in the WWW-Authenticate header, defaults to Endpoint Realm string - // Audience to use when checking jwt based tokens - Audience string // SigningAlgs to use when verifying jwt signatures, defaults to "RS256" & "PS256" SigningAlgs []string - // ClientId to use as username for basic auth against the introspection endpoint - ClientID string - // ClientSecret to use as password for basic auth against the introspection endpoint - ClientSecret string // Insecure can be used to disable http certificate checks Insecure bool - // SkipCheck can be used to further reduce security. Fix that! - SkipChecks bool } // Logger provides a function to set the logger option. @@ -50,13 +42,6 @@ func Realm(r string) Option { } } -// Audience provides a function to set the audience option. -func Audience(a string) Option { - return func(o *Options) { - o.Audience = a - } -} - // SigningAlgs provides a function to set the signing algorithms option. func SigningAlgs(sa []string) Option { return func(o *Options) { @@ -64,30 +49,9 @@ func SigningAlgs(sa []string) Option { } } -// ClientID provides a function to set the client id option. -func ClientID(ci string) Option { - return func(o *Options) { - o.ClientID = ci - } -} - -// ClientSecret provides a function to set the client secret option. -func ClientSecret(cs string) Option { - return func(o *Options) { - o.ClientSecret = cs - } -} - // Insecure provides a function to set the insecure option. func Insecure(i bool) Option { return func(o *Options) { o.Insecure = i } } - -// SkipChecks provides a function to set the ready option. -func SkipChecks(sc bool) Option { - return func(o *Options) { - o.SkipChecks = sc - } -}