Initial QSfera import
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
Iss = "iss"
|
||||
Sub = "sub"
|
||||
Email = "email"
|
||||
Name = "name"
|
||||
PreferredUsername = "preferred_username"
|
||||
UIDNumber = "uidnumber"
|
||||
GIDNumber = "gidnumber"
|
||||
Groups = "groups"
|
||||
QsferaUUID = "qsferauuid"
|
||||
QsferaRoutingPolicy = "qsfera.routing.policy"
|
||||
)
|
||||
|
||||
// SplitWithEscaping splits s into segments using separator which can be escaped using the escape string
|
||||
// See https://codereview.stackexchange.com/a/280193
|
||||
func SplitWithEscaping(s string, separator string, escapeString string) []string {
|
||||
a := strings.Split(s, separator)
|
||||
|
||||
for i := len(a) - 2; i >= 0; i-- {
|
||||
if strings.HasSuffix(a[i], escapeString) {
|
||||
a[i] = a[i][:len(a[i])-len(escapeString)] + separator + a[i+1]
|
||||
a = append(a[:i+1], a[i+2:]...)
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// WalkSegments uses the given array of segments to walk the claims and return whatever interface was found
|
||||
func WalkSegments(segments []string, claims map[string]any) (any, error) {
|
||||
i := 0
|
||||
for ; i < len(segments)-1; i++ {
|
||||
switch castedClaims := claims[segments[i]].(type) {
|
||||
case map[string]any:
|
||||
claims = castedClaims
|
||||
case map[any]any:
|
||||
claims = make(map[string]any, len(castedClaims))
|
||||
for k, v := range castedClaims {
|
||||
if s, ok := k.(string); ok {
|
||||
claims[s] = v
|
||||
} else {
|
||||
return nil, fmt.Errorf("could not walk claims path, key '%v' is not a string", k)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type '%v'", castedClaims)
|
||||
}
|
||||
}
|
||||
return claims[segments[i]], nil
|
||||
}
|
||||
|
||||
// ReadStringClaim returns the string obtained by following the . seperated path in the claims
|
||||
func ReadStringClaim(path string, claims map[string]any) (string, error) {
|
||||
// check the simple case first
|
||||
value, _ := claims[path].(string)
|
||||
if value != "" {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
claim, err := WalkSegments(SplitWithEscaping(path, ".", "\\"), claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if value, _ = claim.(string); value != "" {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
return value, fmt.Errorf("claim path '%s' not set or empty", path)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package oidc_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/qsfera/server/pkg/oidc"
|
||||
)
|
||||
|
||||
type splitWithEscapingTest struct {
|
||||
// Name of the subtest.
|
||||
name string
|
||||
|
||||
// string to split
|
||||
s string
|
||||
|
||||
// seperator to use
|
||||
seperator string
|
||||
|
||||
// escape character to use for escaping
|
||||
escape string
|
||||
|
||||
expectedParts []string
|
||||
}
|
||||
|
||||
func (swet splitWithEscapingTest) run(t *testing.T) {
|
||||
parts := oidc.SplitWithEscaping(swet.s, swet.seperator, swet.escape)
|
||||
if len(swet.expectedParts) != len(parts) {
|
||||
t.Errorf("mismatching length")
|
||||
}
|
||||
for i, v := range swet.expectedParts {
|
||||
if parts[i] != v {
|
||||
t.Errorf("expected part %d to be '%s', got '%s'", i, v, parts[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitWithEscaping(t *testing.T) {
|
||||
tests := []splitWithEscapingTest{
|
||||
{
|
||||
name: "plain claim name",
|
||||
s: "roles",
|
||||
seperator: ".",
|
||||
escape: "\\",
|
||||
expectedParts: []string{"roles"},
|
||||
},
|
||||
{
|
||||
name: "claim with .",
|
||||
s: "my.roles",
|
||||
seperator: ".",
|
||||
escape: "\\",
|
||||
expectedParts: []string{"my", "roles"},
|
||||
},
|
||||
{
|
||||
name: "claim with escaped .",
|
||||
s: "my\\.roles",
|
||||
seperator: ".",
|
||||
escape: "\\",
|
||||
expectedParts: []string{"my.roles"},
|
||||
},
|
||||
{
|
||||
name: "claim with escaped . left",
|
||||
s: "my\\.other.roles",
|
||||
seperator: ".",
|
||||
escape: "\\",
|
||||
expectedParts: []string{"my.other", "roles"},
|
||||
},
|
||||
{
|
||||
name: "claim with escaped . right",
|
||||
s: "my.other\\.roles",
|
||||
seperator: ".",
|
||||
escape: "\\",
|
||||
expectedParts: []string{"my", "other.roles"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.run)
|
||||
}
|
||||
}
|
||||
|
||||
type walkSegmentsTest struct {
|
||||
// Name of the subtest.
|
||||
name string
|
||||
|
||||
// path segments to walk
|
||||
segments []string
|
||||
|
||||
// seperator to use
|
||||
claims map[string]any
|
||||
|
||||
expected any
|
||||
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
func (wst walkSegmentsTest) run(t *testing.T) {
|
||||
v, err := oidc.WalkSegments(wst.segments, wst.claims)
|
||||
if err != nil && !wst.wantErr {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if err == nil && wst.wantErr {
|
||||
t.Errorf("expected error")
|
||||
}
|
||||
if !reflect.DeepEqual(v, wst.expected) {
|
||||
t.Errorf("expected %v got %v", wst.expected, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkSegments(t *testing.T) {
|
||||
byt := []byte(`{"first":{"second":{"third":["value1","value2"]},"foo":"bar"},"fizz":"buzz"}`)
|
||||
var dat map[string]any
|
||||
if err := json.Unmarshal(byt, &dat); err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
|
||||
tests := []walkSegmentsTest{
|
||||
{
|
||||
name: "one segment, single value",
|
||||
segments: []string{"first"},
|
||||
claims: map[string]any{
|
||||
"first": "value",
|
||||
},
|
||||
expected: "value",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "one segment, array value",
|
||||
segments: []string{"first"},
|
||||
claims: map[string]any{
|
||||
"first": []string{"value1", "value2"},
|
||||
},
|
||||
expected: []string{"value1", "value2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "two segments, single value",
|
||||
segments: []string{"first", "second"},
|
||||
claims: map[string]any{
|
||||
"first": map[string]any{
|
||||
"second": "value",
|
||||
},
|
||||
},
|
||||
expected: "value",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "two segments, array value",
|
||||
segments: []string{"first", "second"},
|
||||
claims: map[string]any{
|
||||
"first": map[string]any{
|
||||
"second": []string{"value1", "value2"},
|
||||
},
|
||||
},
|
||||
expected: []string{"value1", "value2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "three segments, array value from json",
|
||||
segments: []string{"first", "second", "third"},
|
||||
claims: dat,
|
||||
expected: []any{"value1", "value2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "three segments, array value with interface key",
|
||||
segments: []string{"first", "second", "third"},
|
||||
claims: map[string]any{
|
||||
"first": map[any]any{
|
||||
"second": map[any]any{
|
||||
"third": []string{"value1", "value2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"value1", "value2"},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.run)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/MicahParks/keyfunc/v2"
|
||||
goidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/proxy/pkg/config"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// OIDCClient used to mock the oidc client during tests
|
||||
type OIDCClient interface {
|
||||
UserInfo(ctx context.Context, ts oauth2.TokenSource) (*UserInfo, error)
|
||||
VerifyAccessToken(ctx context.Context, token string) (RegClaimsWithSID, jwt.MapClaims, error)
|
||||
VerifyLogoutToken(ctx context.Context, token string) (*LogoutToken, error)
|
||||
}
|
||||
|
||||
// KeySet is a set of public JSON Web Keys that can be used to validate the signature
|
||||
// of JSON web tokens. This is expected to be backed by a remote key set through
|
||||
// provider metadata discovery or an in-memory set of keys delivered out-of-band.
|
||||
type KeySet interface {
|
||||
// VerifySignature parses the JSON web token, verifies the signature, and returns
|
||||
// the raw payload. Header and claim fields are validated by other parts of the
|
||||
// package. For example, the KeySet does not need to check values such as signature
|
||||
// algorithm, issuer, and audience since the IDTokenVerifier validates these values
|
||||
// independently.
|
||||
//
|
||||
// If VerifySignature makes HTTP requests to verify the token, it's expected to
|
||||
// use any HTTP client associated with the context through ClientContext.
|
||||
VerifySignature(ctx context.Context, jwt string) (payload []byte, err error)
|
||||
}
|
||||
|
||||
type RegClaimsWithSID struct {
|
||||
SessionID string `json:"sid"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type oidcClient struct {
|
||||
// Logger to use for logging, must be set
|
||||
Logger log.Logger
|
||||
|
||||
issuer string
|
||||
provider *ProviderMetadata
|
||||
providerLock *sync.Mutex
|
||||
skipIssuerValidation bool
|
||||
accessTokenVerifyMethod string
|
||||
remoteKeySet KeySet
|
||||
algorithms []string
|
||||
|
||||
JWKSOptions config.JWKS
|
||||
JWKS *keyfunc.JWKS
|
||||
jwksLock *sync.Mutex
|
||||
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// _supportedAlgorithms is a list of algorithms explicitly supported by this
|
||||
// package. If a provider supports other algorithms, such as HS256 or none,
|
||||
// those values won't be passed to the IDTokenVerifier.
|
||||
var _supportedAlgorithms = map[string]bool{
|
||||
RS256: true,
|
||||
RS384: true,
|
||||
RS512: true,
|
||||
ES256: true,
|
||||
ES384: true,
|
||||
ES512: true,
|
||||
PS256: true,
|
||||
PS384: true,
|
||||
PS512: true,
|
||||
}
|
||||
|
||||
// NewOIDCClient returns an OIDClient instance for the given issuer
|
||||
func NewOIDCClient(opts ...Option) OIDCClient {
|
||||
options := newOptions(opts...)
|
||||
|
||||
return &oidcClient{
|
||||
Logger: options.Logger,
|
||||
issuer: options.OIDCIssuer,
|
||||
httpClient: options.HTTPClient,
|
||||
accessTokenVerifyMethod: options.AccessTokenVerifyMethod,
|
||||
JWKSOptions: options.JWKSOptions, // TODO I don't like that we pass down config options ...
|
||||
JWKS: options.JWKS,
|
||||
providerLock: &sync.Mutex{},
|
||||
jwksLock: &sync.Mutex{},
|
||||
remoteKeySet: options.KeySet,
|
||||
provider: options.ProviderMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *oidcClient) lookupWellKnownOpenidConfiguration(ctx context.Context) error {
|
||||
c.providerLock.Lock()
|
||||
defer c.providerLock.Unlock()
|
||||
if c.provider == nil {
|
||||
wellKnown := strings.TrimSuffix(c.issuer, "/") + wellknownPath
|
||||
req, err := http.NewRequest("GET", wellKnown, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.httpClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read response body: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%s: %s", resp.Status, body)
|
||||
}
|
||||
|
||||
var p ProviderMetadata
|
||||
err = unmarshalResp(resp, body, &p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
|
||||
}
|
||||
|
||||
if !c.skipIssuerValidation && p.Issuer != c.issuer {
|
||||
return fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", c.issuer, p.Issuer)
|
||||
}
|
||||
var algs []string
|
||||
for _, a := range p.IDTokenSigningAlgValuesSupported {
|
||||
if _supportedAlgorithms[a] {
|
||||
algs = append(algs, a)
|
||||
}
|
||||
}
|
||||
c.provider = &p
|
||||
c.algorithms = algs
|
||||
c.remoteKeySet = goidc.NewRemoteKeySet(goidc.ClientContext(ctx, c.httpClient), p.JwksURI)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *oidcClient) getKeyfunc() *keyfunc.JWKS {
|
||||
c.jwksLock.Lock()
|
||||
defer c.jwksLock.Unlock()
|
||||
if c.JWKS == nil {
|
||||
var err error
|
||||
c.Logger.Debug().Str("jwks", c.provider.JwksURI).Msg("discovered jwks endpoint")
|
||||
options := keyfunc.Options{
|
||||
Client: c.httpClient,
|
||||
RefreshErrorHandler: func(err error) {
|
||||
c.Logger.Error().Err(err).Msg("There was an error with the jwt.Keyfunc")
|
||||
},
|
||||
RefreshInterval: time.Minute * time.Duration(c.JWKSOptions.RefreshInterval),
|
||||
RefreshRateLimit: time.Second * time.Duration(c.JWKSOptions.RefreshRateLimit),
|
||||
RefreshTimeout: time.Second * time.Duration(c.JWKSOptions.RefreshTimeout),
|
||||
RefreshUnknownKID: c.JWKSOptions.RefreshUnknownKID,
|
||||
}
|
||||
c.JWKS, err = keyfunc.Get(c.provider.JwksURI, options)
|
||||
if err != nil {
|
||||
c.JWKS = nil
|
||||
c.Logger.Error().Err(err).Msg("Failed to create JWKS from resource at the given URL.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return c.JWKS
|
||||
}
|
||||
|
||||
type stringAsBool bool
|
||||
|
||||
// Claims unmarshals the raw JSON string into a bool.
|
||||
func (sb *stringAsBool) UnmarshalJSON(b []byte) error {
|
||||
v, err := strconv.ParseBool(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*sb = stringAsBool(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserInfo represents the OpenID Connect userinfo claims.
|
||||
type UserInfo struct {
|
||||
Subject string `json:"sub"`
|
||||
Profile string `json:"profile"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
|
||||
claims []byte
|
||||
}
|
||||
|
||||
type userInfoRaw struct {
|
||||
Subject string `json:"sub"`
|
||||
Profile string `json:"profile"`
|
||||
Email string `json:"email"`
|
||||
// Handle providers that return email_verified as a string
|
||||
// https://forums.aws.amazon.com/thread.jspa?messageID=949441󧳁 and
|
||||
// https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11
|
||||
EmailVerified stringAsBool `json:"email_verified"`
|
||||
}
|
||||
|
||||
// Claims unmarshals the raw JSON object claims into the provided object.
|
||||
func (u *UserInfo) Claims(v any) error {
|
||||
if u.claims == nil {
|
||||
return errors.New("oidc: claims not set")
|
||||
}
|
||||
return json.Unmarshal(u.claims, v)
|
||||
}
|
||||
|
||||
// UserInfo retrieves the userinfo from a Token
|
||||
func (c *oidcClient) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*UserInfo, error) {
|
||||
if err := c.lookupWellKnownOpenidConfiguration(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.provider.UserinfoEndpoint == "" {
|
||||
return nil, errors.New("oidc: user info endpoint is not supported by this provider")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", c.provider.UserinfoEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: create GET request: %v", err)
|
||||
}
|
||||
|
||||
token, err := tokenSource.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: get access token: %v", err)
|
||||
}
|
||||
token.SetAuthHeader(req)
|
||||
|
||||
resp, err := c.httpClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s: %s", resp.Status, body)
|
||||
}
|
||||
|
||||
ct := resp.Header.Get("Content-Type")
|
||||
mediaType, _, err := mime.ParseMediaType(ct)
|
||||
if err == nil && mediaType == "application/jwt" {
|
||||
payload, err := c.remoteKeySet.VerifySignature(goidc.ClientContext(ctx, c.httpClient), string(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: invalid userinfo jwt signature %v", err)
|
||||
}
|
||||
body = payload
|
||||
}
|
||||
|
||||
var userInfo userInfoRaw
|
||||
if err := json.Unmarshal(body, &userInfo); err != nil {
|
||||
return nil, fmt.Errorf("oidc: failed to decode userinfo: %v", err)
|
||||
}
|
||||
return &UserInfo{
|
||||
Subject: userInfo.Subject,
|
||||
Profile: userInfo.Profile,
|
||||
Email: userInfo.Email,
|
||||
EmailVerified: bool(userInfo.EmailVerified),
|
||||
claims: body,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *oidcClient) VerifyAccessToken(ctx context.Context, token string) (RegClaimsWithSID, jwt.MapClaims, error) {
|
||||
if err := c.lookupWellKnownOpenidConfiguration(ctx); err != nil {
|
||||
return RegClaimsWithSID{}, jwt.MapClaims{}, err
|
||||
}
|
||||
switch c.accessTokenVerifyMethod {
|
||||
case config.AccessTokenVerificationJWT:
|
||||
return c.verifyAccessTokenJWT(token)
|
||||
case config.AccessTokenVerificationNone:
|
||||
c.Logger.Debug().Msg("Access Token verification disabled")
|
||||
return RegClaimsWithSID{}, jwt.MapClaims{}, nil
|
||||
default:
|
||||
c.Logger.Error().Str("access_token_verify_method", c.accessTokenVerifyMethod).Msg("Unknown Access Token verification setting")
|
||||
return RegClaimsWithSID{}, jwt.MapClaims{}, errors.New("unknown Access Token Verification method")
|
||||
}
|
||||
}
|
||||
|
||||
// verifyAccessTokenJWT tries to parse and verify the access token as a JWT.
|
||||
func (c *oidcClient) verifyAccessTokenJWT(token string) (RegClaimsWithSID, jwt.MapClaims, error) {
|
||||
var claims RegClaimsWithSID
|
||||
mapClaims := jwt.MapClaims{}
|
||||
jwks := c.getKeyfunc()
|
||||
if jwks == nil {
|
||||
return claims, mapClaims, errors.New("error initializing jwks keyfunc")
|
||||
}
|
||||
|
||||
issuer := c.issuer
|
||||
if c.provider.AccessTokenIssuer != "" {
|
||||
// AD FS .well-known/openid-configuration has an optional `access_token_issuer` which takes precedence over `issuer`
|
||||
// See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-oidce/586de7dd-3385-47c7-93a2-935d9e90441c
|
||||
issuer = c.provider.AccessTokenIssuer
|
||||
}
|
||||
|
||||
_, err := jwt.ParseWithClaims(token, &claims, jwks.Keyfunc, jwt.WithIssuer(issuer))
|
||||
if err != nil {
|
||||
return claims, mapClaims, err
|
||||
}
|
||||
_, _, err = new(jwt.Parser).ParseUnverified(token, mapClaims)
|
||||
// TODO: decode mapClaims to sth readable
|
||||
c.Logger.Debug().Interface("access token", &claims).Msg("parsed access token")
|
||||
if err != nil {
|
||||
c.Logger.Info().Err(err).Msg("Failed to parse/verify the access token.")
|
||||
return claims, mapClaims, err
|
||||
}
|
||||
|
||||
return claims, mapClaims, nil
|
||||
}
|
||||
|
||||
func (c *oidcClient) VerifyLogoutToken(ctx context.Context, rawToken string) (*LogoutToken, error) {
|
||||
var claims LogoutToken
|
||||
if err := c.lookupWellKnownOpenidConfiguration(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jwks := c.getKeyfunc()
|
||||
if jwks == nil {
|
||||
return nil, errors.New("error initializing jwks keyfunc")
|
||||
}
|
||||
|
||||
// From the backchannel-logout spec: Like ID Tokens, selection of the
|
||||
// algorithm used is governed by the id_token_signing_alg_values_supported
|
||||
// Discovery parameter and the id_token_signed_response_alg Registration
|
||||
// parameter when they are used; otherwise, the value SHOULD be the default
|
||||
// of RS256
|
||||
supportedSigAlgs := c.algorithms
|
||||
if len(supportedSigAlgs) == 0 {
|
||||
supportedSigAlgs = []string{RS256}
|
||||
}
|
||||
|
||||
_, err := jwt.ParseWithClaims(rawToken, &claims, jwks.Keyfunc, jwt.WithValidMethods(supportedSigAlgs), jwt.WithIssuer(c.issuer))
|
||||
if err != nil {
|
||||
c.Logger.Debug().Err(err).Msg("Failed to parse logout token")
|
||||
return nil, err
|
||||
}
|
||||
// Basic token validation has happened in ParseWithClaims (signature,
|
||||
// issuer, audience, ...). Now for some logout token specific checks.
|
||||
// 1. Verify that the Logout Token contains a sub Claim, a sid Claim, or both.
|
||||
if claims.Subject == "" && claims.SessionId == "" {
|
||||
return nil, fmt.Errorf("oidc: logout token must contain either sub or sid and MAY contain both")
|
||||
}
|
||||
// 2. 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 claims.Events.Event == nil {
|
||||
return nil, fmt.Errorf("oidc: logout token must contain logout event")
|
||||
}
|
||||
// 3. Verify that the Logout Token does not contain a nonce Claim.
|
||||
if claims.Nonce != nil {
|
||||
return nil, fmt.Errorf("oidc: nonce on logout token MUST NOT be present")
|
||||
}
|
||||
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
func unmarshalResp(r *http.Response, body []byte, v any) error {
|
||||
err := json.Unmarshal(body, &v)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
ct := r.Header.Get("Content-Type")
|
||||
mediaType, _, err := mime.ParseMediaType(ct)
|
||||
if err == nil && mediaType == "application/json" {
|
||||
return fmt.Errorf("got Content-Type = application/json, but could not unmarshal as JSON: %v", err)
|
||||
}
|
||||
return fmt.Errorf("expected Content-Type = application/json, got %q: %v", ct, err)
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package oidc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"testing"
|
||||
|
||||
"github.com/MicahParks/keyfunc/v2"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/qsfera/server/pkg/oidc"
|
||||
)
|
||||
|
||||
type signingKey struct {
|
||||
priv any
|
||||
jwks *keyfunc.JWKS
|
||||
}
|
||||
|
||||
func TestLogoutVerify(t *testing.T) {
|
||||
tests := []logoutVerificationTest{
|
||||
{
|
||||
name: "good token",
|
||||
logoutToken: jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
|
||||
"iss": "https://foo",
|
||||
"sub": "248289761001",
|
||||
"aud": "s6BhdRkqt3",
|
||||
"iat": 1471566154,
|
||||
"jti": "bWJq",
|
||||
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
|
||||
"events": map[string]any{
|
||||
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
|
||||
},
|
||||
}),
|
||||
signKey: newRSAKey(t),
|
||||
},
|
||||
{
|
||||
name: "invalid issuer",
|
||||
issuer: "https://bar",
|
||||
logoutToken: jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
|
||||
"iss": "https://foo1",
|
||||
"sub": "248289761001",
|
||||
"events": map[string]any{
|
||||
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
|
||||
},
|
||||
}),
|
||||
signKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid sig",
|
||||
logoutToken: jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
|
||||
"iss": "https://foo",
|
||||
"sub": "248289761001",
|
||||
"aud": "s6BhdRkqt3",
|
||||
"iat": 1471566154,
|
||||
"jti": "bWJq",
|
||||
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
|
||||
"events": map[string]any{
|
||||
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
|
||||
},
|
||||
}),
|
||||
signKey: newRSAKey(t),
|
||||
verificationKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no sid and no sub",
|
||||
logoutToken: jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
|
||||
"iss": "https://foo",
|
||||
"aud": "s6BhdRkqt3",
|
||||
"iat": 1471566154,
|
||||
"jti": "bWJq",
|
||||
"events": map[string]any{
|
||||
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
|
||||
},
|
||||
}),
|
||||
signKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Prohibited nonce present",
|
||||
logoutToken: jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
|
||||
"iss": "https://foo",
|
||||
"sub": "248289761001",
|
||||
"aud": "s6BhdRkqt3",
|
||||
"iat": 1471566154,
|
||||
"jti": "bWJq",
|
||||
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
|
||||
"nonce": "123",
|
||||
"events": map[string]any{
|
||||
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
|
||||
},
|
||||
}),
|
||||
signKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Wrong Event string",
|
||||
logoutToken: jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
|
||||
"iss": "https://foo",
|
||||
"sub": "248289761001",
|
||||
"aud": "s6BhdRkqt3",
|
||||
"iat": 1471566154,
|
||||
"jti": "bWJq",
|
||||
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
|
||||
"events": map[string]any{
|
||||
"http://blah.blah.blash/event/backchannel-logout": struct{}{},
|
||||
},
|
||||
}),
|
||||
signKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "No Event string",
|
||||
logoutToken: jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
|
||||
"iss": "https://foo",
|
||||
"sub": "248289761001",
|
||||
"aud": "s6BhdRkqt3",
|
||||
"iat": 1471566154,
|
||||
"jti": "bWJq",
|
||||
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
|
||||
}),
|
||||
signKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.run)
|
||||
}
|
||||
}
|
||||
|
||||
type logoutVerificationTest struct {
|
||||
// Name of the subtest.
|
||||
name string
|
||||
|
||||
// If not provided defaults to "https://foo"
|
||||
issuer string
|
||||
|
||||
// JWT payload (just the claims).
|
||||
logoutToken *jwt.Token
|
||||
|
||||
// Key to sign the ID Token with.
|
||||
signKey *signingKey
|
||||
// If not provided defaults to signKey. Only useful when
|
||||
// testing invalid signatures.
|
||||
verificationKey *signingKey
|
||||
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
func (v logoutVerificationTest) runGetToken(t *testing.T) (*oidc.LogoutToken, error) {
|
||||
// token := v.signKey.sign(t, []byte(v.logoutToken))
|
||||
v.logoutToken.Header["kid"] = "1"
|
||||
token, err := v.logoutToken.SignedString(v.signKey.priv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
issuer := "https://foo"
|
||||
var jwks *keyfunc.JWKS
|
||||
if v.verificationKey == nil {
|
||||
jwks = v.signKey.jwks
|
||||
} else {
|
||||
jwks = v.verificationKey.jwks
|
||||
}
|
||||
|
||||
pm := oidc.ProviderMetadata{}
|
||||
verifier := oidc.NewOIDCClient(
|
||||
oidc.WithOidcIssuer(issuer),
|
||||
oidc.WithJWKS(jwks),
|
||||
oidc.WithProviderMetadata(&pm),
|
||||
)
|
||||
|
||||
return verifier.VerifyLogoutToken(ctx, token)
|
||||
}
|
||||
|
||||
func (l logoutVerificationTest) run(t *testing.T) {
|
||||
_, err := l.runGetToken(t)
|
||||
if err != nil && !l.wantErr {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if err == nil && l.wantErr {
|
||||
t.Errorf("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func newRSAKey(t testing.TB) *signingKey {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 1028)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
givenKey := keyfunc.NewGivenRSA(
|
||||
&priv.PublicKey,
|
||||
keyfunc.GivenKeyOptions{Algorithm: jwt.SigningMethodRS256.Alg()},
|
||||
)
|
||||
jwks := keyfunc.NewGiven(
|
||||
map[string]keyfunc.GivenKey{
|
||||
"1": givenKey,
|
||||
},
|
||||
)
|
||||
|
||||
return &signingKey{priv, jwks}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package oidc
|
||||
|
||||
import "context"
|
||||
|
||||
// contextKey is the key for oidc claims in a context
|
||||
type contextKey struct{}
|
||||
|
||||
// newSessionFlagKey is the key for the new session flag in a context
|
||||
type newSessionFlagKey struct{}
|
||||
|
||||
// NewContext makes a new context that contains the OpenID connect claims in a map.
|
||||
func NewContext(parent context.Context, c map[string]any) context.Context {
|
||||
return context.WithValue(parent, contextKey{}, c)
|
||||
}
|
||||
|
||||
// FromContext returns the claims map stored in a context, or nil if there isn't one.
|
||||
func FromContext(ctx context.Context) map[string]any {
|
||||
s, _ := ctx.Value(contextKey{}).(map[string]any)
|
||||
return s
|
||||
}
|
||||
|
||||
// NewContextSessionFlag makes a new context that contains the new session flag.
|
||||
func NewContextSessionFlag(ctx context.Context, flag bool) context.Context {
|
||||
return context.WithValue(ctx, newSessionFlagKey{}, flag)
|
||||
}
|
||||
|
||||
// NewSessionFlagFromContext returns the new session flag stored in a context.
|
||||
func NewSessionFlagFromContext(ctx context.Context) bool {
|
||||
s, _ := ctx.Value(newSessionFlagKey{}).(bool)
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package oidc
|
||||
|
||||
// JOSE asymmetric signing algorithm values as defined by RFC 7518
|
||||
//
|
||||
// see: https://tools.ietf.org/html/rfc7518#section-3.1
|
||||
const (
|
||||
RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256
|
||||
RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384
|
||||
RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512
|
||||
ES256 = "ES256" // ECDSA using P-256 and SHA-256
|
||||
ES384 = "ES384" // ECDSA using P-384 and SHA-384
|
||||
ES512 = "ES512" // ECDSA using P-521 and SHA-512
|
||||
PS256 = "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256
|
||||
PS384 = "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384
|
||||
PS512 = "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
)
|
||||
|
||||
const wellknownPath = "/.well-known/openid-configuration"
|
||||
|
||||
// The ProviderMetadata describes an idp.
|
||||
// see https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
type ProviderMetadata struct {
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"`
|
||||
//claims_parameter_supported
|
||||
ClaimsSupported []string `json:"claims_supported,omitempty"`
|
||||
//grant_types_supported
|
||||
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
// AccessTokenIssuer is only used by AD FS and needs to be used when validating the iss of its access tokens
|
||||
// See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-oidce/586de7dd-3385-47c7-93a2-935d9e90441c
|
||||
AccessTokenIssuer string `json:"access_token_issuer,omitempty"`
|
||||
JwksURI string `json:"jwks_uri,omitempty"`
|
||||
//registration_endpoint
|
||||
//request_object_signing_alg_values_supported
|
||||
//request_parameter_supported
|
||||
//request_uri_parameter_supported
|
||||
//require_request_uri_registration
|
||||
//response_modes_supported
|
||||
ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
|
||||
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
||||
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
|
||||
TokenEndpoint string `json:"token_endpoint,omitempty"`
|
||||
//token_endpoint_auth_methods_supported
|
||||
//token_endpoint_auth_signing_alg_values_supported
|
||||
UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"`
|
||||
//userinfo_signing_alg_values_supported
|
||||
//code_challenge_methods_supported
|
||||
IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`
|
||||
//introspection_endpoint_auth_methods_supported
|
||||
//introspection_endpoint_auth_signing_alg_values_supported
|
||||
RevocationEndpoint string `json:"revocation_endpoint,omitempty"`
|
||||
//revocation_endpoint_auth_methods_supported
|
||||
//revocation_endpoint_auth_signing_alg_values_supported
|
||||
//id_token_encryption_alg_values_supported
|
||||
//id_token_encryption_enc_values_supported
|
||||
//userinfo_encryption_alg_values_supported
|
||||
//userinfo_encryption_enc_values_supported
|
||||
//request_object_encryption_alg_values_supported
|
||||
//request_object_encryption_enc_values_supported
|
||||
CheckSessionIframe string `json:"check_session_iframe,omitempty"`
|
||||
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
||||
//claim_types_supported
|
||||
}
|
||||
|
||||
// Logout Token defines an logout Token
|
||||
type LogoutToken struct {
|
||||
jwt.RegisteredClaims
|
||||
// The Session Id
|
||||
SessionId string `json:"sid"`
|
||||
Events LogoutEvent `json:"events"`
|
||||
// Note: This is just here to be able to check for nonce being absent
|
||||
Nonce *string `json:"nonce"`
|
||||
}
|
||||
|
||||
// LogoutEvent defines a logout Event
|
||||
type LogoutEvent struct {
|
||||
Event *struct{} `json:"http://schemas.openid.net/event/backchannel-logout"`
|
||||
}
|
||||
|
||||
func GetIDPMetadata(logger log.Logger, client *http.Client, idpURI string) (ProviderMetadata, error) {
|
||||
wellknownURI := strings.TrimSuffix(idpURI, "/") + wellknownPath
|
||||
|
||||
resp, err := client.Get(wellknownURI)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to set request for .well-known/openid-configuration")
|
||||
return ProviderMetadata{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("unable to read discovery response body")
|
||||
return ProviderMetadata{}, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Error().Str("status", resp.Status).Str("body", string(body)).Msg("error requesting openid-configuration")
|
||||
return ProviderMetadata{}, err
|
||||
}
|
||||
|
||||
var oidcMetadata ProviderMetadata
|
||||
err = json.Unmarshal(body, &oidcMetadata)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to decode provider openid-configuration")
|
||||
return ProviderMetadata{}, err
|
||||
}
|
||||
return oidcMetadata, nil
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/qsfera/server/pkg/oidc"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// NewOIDCClient creates a new instance of OIDCClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewOIDCClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *OIDCClient {
|
||||
mock := &OIDCClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// OIDCClient is an autogenerated mock type for the OIDCClient type
|
||||
type OIDCClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type OIDCClient_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *OIDCClient) EXPECT() *OIDCClient_Expecter {
|
||||
return &OIDCClient_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// UserInfo provides a mock function for the type OIDCClient
|
||||
func (_mock *OIDCClient) UserInfo(ctx context.Context, ts oauth2.TokenSource) (*oidc.UserInfo, error) {
|
||||
ret := _mock.Called(ctx, ts)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UserInfo")
|
||||
}
|
||||
|
||||
var r0 *oidc.UserInfo
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, oauth2.TokenSource) (*oidc.UserInfo, error)); ok {
|
||||
return returnFunc(ctx, ts)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, oauth2.TokenSource) *oidc.UserInfo); ok {
|
||||
r0 = returnFunc(ctx, ts)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*oidc.UserInfo)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, oauth2.TokenSource) error); ok {
|
||||
r1 = returnFunc(ctx, ts)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// OIDCClient_UserInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UserInfo'
|
||||
type OIDCClient_UserInfo_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UserInfo is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - ts oauth2.TokenSource
|
||||
func (_e *OIDCClient_Expecter) UserInfo(ctx interface{}, ts interface{}) *OIDCClient_UserInfo_Call {
|
||||
return &OIDCClient_UserInfo_Call{Call: _e.mock.On("UserInfo", ctx, ts)}
|
||||
}
|
||||
|
||||
func (_c *OIDCClient_UserInfo_Call) Run(run func(ctx context.Context, ts oauth2.TokenSource)) *OIDCClient_UserInfo_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 oauth2.TokenSource
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(oauth2.TokenSource)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OIDCClient_UserInfo_Call) Return(userInfo *oidc.UserInfo, err error) *OIDCClient_UserInfo_Call {
|
||||
_c.Call.Return(userInfo, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OIDCClient_UserInfo_Call) RunAndReturn(run func(ctx context.Context, ts oauth2.TokenSource) (*oidc.UserInfo, error)) *OIDCClient_UserInfo_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// VerifyAccessToken provides a mock function for the type OIDCClient
|
||||
func (_mock *OIDCClient) VerifyAccessToken(ctx context.Context, token string) (oidc.RegClaimsWithSID, jwt.MapClaims, error) {
|
||||
ret := _mock.Called(ctx, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for VerifyAccessToken")
|
||||
}
|
||||
|
||||
var r0 oidc.RegClaimsWithSID
|
||||
var r1 jwt.MapClaims
|
||||
var r2 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (oidc.RegClaimsWithSID, jwt.MapClaims, error)); ok {
|
||||
return returnFunc(ctx, token)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) oidc.RegClaimsWithSID); ok {
|
||||
r0 = returnFunc(ctx, token)
|
||||
} else {
|
||||
r0 = ret.Get(0).(oidc.RegClaimsWithSID)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string) jwt.MapClaims); ok {
|
||||
r1 = returnFunc(ctx, token)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(jwt.MapClaims)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(2).(func(context.Context, string) error); ok {
|
||||
r2 = returnFunc(ctx, token)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// OIDCClient_VerifyAccessToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyAccessToken'
|
||||
type OIDCClient_VerifyAccessToken_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// VerifyAccessToken is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - token string
|
||||
func (_e *OIDCClient_Expecter) VerifyAccessToken(ctx interface{}, token interface{}) *OIDCClient_VerifyAccessToken_Call {
|
||||
return &OIDCClient_VerifyAccessToken_Call{Call: _e.mock.On("VerifyAccessToken", ctx, token)}
|
||||
}
|
||||
|
||||
func (_c *OIDCClient_VerifyAccessToken_Call) Run(run func(ctx context.Context, token string)) *OIDCClient_VerifyAccessToken_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OIDCClient_VerifyAccessToken_Call) Return(regClaimsWithSID oidc.RegClaimsWithSID, mapClaims jwt.MapClaims, err error) *OIDCClient_VerifyAccessToken_Call {
|
||||
_c.Call.Return(regClaimsWithSID, mapClaims, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OIDCClient_VerifyAccessToken_Call) RunAndReturn(run func(ctx context.Context, token string) (oidc.RegClaimsWithSID, jwt.MapClaims, error)) *OIDCClient_VerifyAccessToken_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// VerifyLogoutToken provides a mock function for the type OIDCClient
|
||||
func (_mock *OIDCClient) VerifyLogoutToken(ctx context.Context, token string) (*oidc.LogoutToken, error) {
|
||||
ret := _mock.Called(ctx, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for VerifyLogoutToken")
|
||||
}
|
||||
|
||||
var r0 *oidc.LogoutToken
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*oidc.LogoutToken, error)); ok {
|
||||
return returnFunc(ctx, token)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) *oidc.LogoutToken); ok {
|
||||
r0 = returnFunc(ctx, token)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*oidc.LogoutToken)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = returnFunc(ctx, token)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// OIDCClient_VerifyLogoutToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyLogoutToken'
|
||||
type OIDCClient_VerifyLogoutToken_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// VerifyLogoutToken is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - token string
|
||||
func (_e *OIDCClient_Expecter) VerifyLogoutToken(ctx interface{}, token interface{}) *OIDCClient_VerifyLogoutToken_Call {
|
||||
return &OIDCClient_VerifyLogoutToken_Call{Call: _e.mock.On("VerifyLogoutToken", ctx, token)}
|
||||
}
|
||||
|
||||
func (_c *OIDCClient_VerifyLogoutToken_Call) Run(run func(ctx context.Context, token string)) *OIDCClient_VerifyLogoutToken_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OIDCClient_VerifyLogoutToken_Call) Return(logoutToken *oidc.LogoutToken, err error) *OIDCClient_VerifyLogoutToken_Call {
|
||||
_c.Call.Return(logoutToken, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OIDCClient_VerifyLogoutToken_Call) RunAndReturn(run func(ctx context.Context, token string) (*oidc.LogoutToken, error)) *OIDCClient_VerifyLogoutToken_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/MicahParks/keyfunc/v2"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/proxy/pkg/config"
|
||||
|
||||
goidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
// HTTPClient to use for requests
|
||||
HTTPClient *http.Client
|
||||
// Logger to use for logging, must be set
|
||||
Logger log.Logger
|
||||
// The OpenID Connect Issuer URL
|
||||
OIDCIssuer string
|
||||
// JWKSOptions to use when retrieving keys
|
||||
JWKSOptions config.JWKS
|
||||
// the JWKS keyset to use for verifying signatures of Access- and
|
||||
// Logout-Tokens
|
||||
// this option is mostly needed for unit test. To avoid fetching the keys
|
||||
// from the issuer
|
||||
JWKS *keyfunc.JWKS
|
||||
// KeySet to use when verifiing signatures of jwt encoded
|
||||
// user info responses
|
||||
// TODO move userinfo verification to use jwt/keyfunc as well
|
||||
KeySet KeySet
|
||||
// AccessTokenVerifyMethod to use when verifying access tokens
|
||||
// TODO pass a function or interface to verify? an AccessTokenVerifier?
|
||||
AccessTokenVerifyMethod string
|
||||
// Config to use
|
||||
Config *goidc.Config
|
||||
|
||||
// ProviderMetadata to use
|
||||
ProviderMetadata *ProviderMetadata
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithOidcIssuer provides a function to set the openid connect issuer option.
|
||||
func WithOidcIssuer(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.OIDCIssuer = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger provides a function to set the logger option.
|
||||
func WithLogger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithAccessTokenVerifyMethod provides a function to set the accessTokenVerifyMethod option.
|
||||
func WithAccessTokenVerifyMethod(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.AccessTokenVerifyMethod = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithHTTPClient provides a function to set the httpClient option.
|
||||
func WithHTTPClient(val *http.Client) Option {
|
||||
return func(o *Options) {
|
||||
o.HTTPClient = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithJWKSOptions provides a function to set the jwksOptions option.
|
||||
func WithJWKSOptions(val config.JWKS) Option {
|
||||
return func(o *Options) {
|
||||
o.JWKSOptions = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithJWKS provides a function to set the JWKS option (mainly useful for testing).
|
||||
func WithJWKS(val *keyfunc.JWKS) Option {
|
||||
return func(o *Options) {
|
||||
o.JWKS = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithKeySet provides a function to set the KeySet option.
|
||||
func WithKeySet(val KeySet) Option {
|
||||
return func(o *Options) {
|
||||
o.KeySet = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithConfig provides a function to set the Config option.
|
||||
func WithConfig(val *goidc.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithProviderMetadata provides a function to set the provider option.
|
||||
func WithProviderMetadata(val *ProviderMetadata) Option {
|
||||
return func(o *Options) {
|
||||
o.ProviderMetadata = val
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user