Merge pull request #15 from owncloud/oidc-middleware

fix and simplify oidc middleware
This commit is contained in:
Jörn Friedrich Dreyer
2019-12-16 12:07:18 +01:00
committed by GitHub
5 changed files with 16 additions and 225 deletions

1
go.mod
View File

@@ -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

3
go.sum
View File

@@ -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=

View File

@@ -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))

View File

@@ -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"`
}

View File

@@ -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
}
}