work on logout

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
Jörn Friedrich Dreyer
2023-04-19 17:32:25 +02:00
committed by Christian Richter
parent b608d0b0f9
commit bc15b8a396
5 changed files with 241 additions and 154 deletions
+86 -1
View File
@@ -1,6 +1,7 @@
package oidc
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -18,12 +19,14 @@ import (
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
"golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2"
)
// OIDCProvider used to mock the oidc provider during tests
type OIDCProvider interface {
UserInfo(ctx context.Context, ts oauth2.TokenSource) (*UserInfo, error)
VerifyAccessToken(ctx context.Context, token string) (jwt.RegisteredClaims, []string, error)
VerifyLogoutToken(ctx context.Context, token string) (*LogoutToken, error)
}
// KeySet is a set of publc JSON Web Keys that can be used to validate the signature
@@ -279,13 +282,95 @@ func (c *oidcClient) VerifyAccessToken(ctx context.Context, token string) (jwt.R
}
}
func (c *oidcClient) VerifyLogoutToken(ctx context.Context, rawIDToken string) (*LogoutToken, error) {
jws, err := jose.ParseSigned(rawIDToken)
if err != nil {
return nil, err
}
// Throw out tokens with invalid claims before trying to verify the token. This lets
// us do cheap checks before possibly re-syncing keys.
payload, err := parseJWT(rawIDToken)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
var token LogoutToken
if err := json.Unmarshal(payload, &token); err != nil {
return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err)
}
//4. Verify that the Logout Token contains a sub Claim, a sid Claim, or both.
if token.Subject == "" && token.SessionId == "" {
return nil, fmt.Errorf("oidc: logout token must contain either sub or sid and MAY contain both")
}
//5. Verify that the Logout Token contains an events Claim whose value is JSON object containing the member name http://schemas.openid.net/event/backchannel-logout.
if token.Events.Event == nil {
return nil, fmt.Errorf("oidc: logout token must contain logout event")
}
//6. Verify that the Logout Token does not contain a nonce Claim.
type nonce struct {
Nonce *string `json:"nonce"`
}
var n nonce
json.Unmarshal(payload, &n)
if n.Nonce != nil {
return nil, fmt.Errorf("oidc: nonce on logout token MUST NOT be present")
}
// Check issuer.
if !v.config.SkipIssuerCheck && token.Issuer != v.issuer {
return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.issuer, token.Issuer)
}
// If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty.
//
// This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party).
if !v.config.SkipClientIDCheck {
if v.config.ClientID != "" {
if !contains(token.Audience, v.config.ClientID) {
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, token.Audience)
}
} else {
return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set")
}
}
switch len(jws.Signatures) {
case 0:
return nil, fmt.Errorf("oidc: id token not signed")
case 1:
default:
return nil, fmt.Errorf("oidc: multiple signatures on id token not supported")
}
sig := jws.Signatures[0]
supportedSigAlgs := v.config.SupportedSigningAlgs
if len(supportedSigAlgs) == 0 {
supportedSigAlgs = []string{gOidc.RS256}
}
if !contains(supportedSigAlgs, sig.Header.Algorithm) {
return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm)
}
gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("failed to verify signature: %v", err)
}
// Ensure that the payload returned by the square actually matches the payload parsed earlier.
if !bytes.Equal(gotPayload, payload) {
return nil, errors.New("oidc: internal error, payload parsed did not match previous payload")
}
return &token, nil
}
// verifyAccessTokenJWT tries to parse and verify the access token as a JWT.
func (c *oidcClient) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, []string, error) {
var claims jwt.RegisteredClaims
var mapClaims []string
jwks := c.getKeyfunc()
if jwks == nil {
return claims, mapClaims, errors.New("Error initializing jwks keyfunc")
return claims, mapClaims, errors.New("error initializing jwks keyfunc")
}
_, err := jwt.ParseWithClaims(token, &claims, jwks.Keyfunc)
+3 -98
View File
@@ -11,97 +11,11 @@ import (
"gopkg.in/square/go-jose.v2"
"time"
gOidc "github.com/coreos/go-oidc/v3/oidc"
)
// This adds the ability to verify Logout Tokens as specified in https://openid.net/specs/openid-connect-backchannel-1_0.html
type logoutEvent struct {
Event *struct{} `json:"http://schemas.openid.net/event/backchannel-logout"`
}
type audience []string
func (a *audience) UnmarshalJSON(b []byte) error {
var s string
if json.Unmarshal(b, &s) == nil {
*a = audience{s}
return nil
}
var auds []string
if err := json.Unmarshal(b, &auds); err != nil {
return err
}
*a = auds
return nil
}
type jsonTime time.Time
func (j *jsonTime) UnmarshalJSON(b []byte) error {
var n json.Number
if err := json.Unmarshal(b, &n); err != nil {
return err
}
var unix int64
if t, err := n.Int64(); err == nil {
unix = t
} else {
f, err := n.Float64()
if err != nil {
return err
}
unix = int64(f)
}
*j = jsonTime(time.Unix(unix, 0))
return nil
}
// logoutToken
type logoutToken struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience audience `json:"aud"`
IssuedAt jsonTime `json:"iat"`
JwtID string `json:"jti"`
Events logoutEvent `json:"events"`
Sid string `json:"sid"`
}
// Logout Token
type LogoutToken struct {
// The URL of the server which issued this token. OpenID Connect
// requires this value always be identical to the URL used for
// initial discovery.
//
// Note: Because of a known issue with Google Accounts' implementation
// this value may differ when using Google.
//
// See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo
Issuer string
// A unique string which identifies the end user.
Subject string
// The client ID, or set of client IDs, that this token is issued for. For
// common uses, this is the client that initialized the auth flow.
//
// This package ensures the audience contains an expected value.
Audience []string
// When the token was issued by the provider.
IssuedAt time.Time
// The Session Id
SessionId string
// Jwt Id
JwtID string
}
// LogoutTokenVerifier provides verification for Logout Tokens.
type LogoutTokenVerifier struct {
keySet gOidc.KeySet
@@ -136,13 +50,13 @@ func (v *LogoutTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*L
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
var token logoutToken
var token LogoutToken
if err := json.Unmarshal(payload, &token); err != nil {
return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err)
}
//4. Verify that the Logout Token contains a sub Claim, a sid Claim, or both.
if token.Subject == "" && token.Sid == "" {
if token.Subject == "" && token.SessionId == "" {
return nil, fmt.Errorf("oidc: logout token must contain either sub or sid and MAY contain both")
}
//5. Verify that the Logout Token contains an events Claim whose value is JSON object containing the member name http://schemas.openid.net/event/backchannel-logout.
@@ -204,16 +118,7 @@ func (v *LogoutTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*L
return nil, errors.New("oidc: internal error, payload parsed did not match previous payload")
}
t := &LogoutToken{
Issuer: token.Issuer,
Subject: token.Subject,
Audience: token.Audience,
IssuedAt: time.Time(token.IssuedAt),
SessionId: token.Sid,
JwtID: token.JwtID,
}
return t, nil
return &token, nil
}
func parseJWT(p string) ([]byte, error) {
+76
View File
@@ -5,6 +5,7 @@ import (
"io"
"net/http"
"strings"
"time"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
@@ -53,6 +54,81 @@ type ProviderMetadata struct {
//claim_types_supported
}
// Logout Token
type LogoutToken struct {
// The URL of the server which issued this token. OpenID Connect
// requires this value always be identical to the URL used for
// initial discovery.
//
// Note: Because of a known issue with Google Accounts' implementation
// this value may differ when using Google.
//
// See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo
Issuer string `json:iss` // example "https://server.example.com"
// A unique string which identifies the end user.
Subject string `json:sub` //"248289761001"
// The client ID, or set of client IDs, that this token is issued for. For
// common uses, this is the client that initialized the auth flow.
//
// This package ensures the audience contains an expected value.
Audience audience `json:aud` // "s6BhdRkqt3"
// When the token was issued by the provider.
IssuedAt jsonTime `json:"iat"`
// The Session Id
SessionId string `json:"sid"`
Events logoutEvent `json:"events"`
// Jwt Id
JwtID string `json:"jti"`
}
type logoutEvent struct {
Event *struct{} `json:"http://schemas.openid.net/event/backchannel-logout"`
}
type audience []string
func (a *audience) UnmarshalJSON(b []byte) error {
var s string
if json.Unmarshal(b, &s) == nil {
*a = audience{s}
return nil
}
var auds []string
if err := json.Unmarshal(b, &auds); err != nil {
return err
}
*a = auds
return nil
}
type jsonTime time.Time
func (j *jsonTime) UnmarshalJSON(b []byte) error {
var n json.Number
if err := json.Unmarshal(b, &n); err != nil {
return err
}
var unix int64
if t, err := n.Int64(); err == nil {
unix = t
} else {
f, err := n.Float64()
if err != nil {
return err
}
unix = int64(f)
}
*j = jsonTime(time.Unix(unix, 0))
return nil
}
func GetIDPMetadata(logger log.Logger, client *http.Client, idpURI string) (ProviderMetadata, error) {
wellknownURI := strings.TrimSuffix(idpURI, "/") + wellknownPath