simplify oidc middleware

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
Jörn Friedrich Dreyer
2019-12-16 11:59:37 +01:00
parent d448ca547c
commit 1000a04b91
5 changed files with 16 additions and 225 deletions
+16 -130
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))