mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-28 16:01:18 -05:00
Merge pull request #15 from owncloud/oidc-middleware
fix and simplify oidc middleware
This commit is contained in:
1
go.mod
1
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
|
||||
|
||||
3
go.sum
3
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=
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user