Merge pull request #6007 from dragonchaser/backchannellogout
[full-ci] Implement backchannel logout
This commit is contained in:
@@ -11,6 +11,7 @@ require (
|
||||
github.com/armon/go-radix v1.0.0
|
||||
github.com/bbalet/stopwords v1.0.0
|
||||
github.com/blevesearch/bleve/v2 v2.3.7
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/coreos/go-oidc/v3 v3.4.0
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20221012090518-ef2996678965
|
||||
github.com/cs3org/reva/v2 v2.12.1-0.20230417084429-b3d96f9db80c
|
||||
@@ -100,6 +101,7 @@ require (
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f
|
||||
google.golang.org/grpc v1.54.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gotest.tools/v3 v3.4.0
|
||||
stash.kopano.io/kgol/oidc-go v0.3.4
|
||||
@@ -153,7 +155,6 @@ require (
|
||||
github.com/cilium/ebpf v0.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.2.0 // indirect
|
||||
github.com/containerd/cgroups v1.0.4 // indirect
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
@@ -313,7 +314,6 @@ require (
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -629,6 +629,8 @@ github.com/crewjam/saml v0.4.13 h1:TYHggH/hwP7eArqiXSJUvtOPNzQDyQ7vwmwEqlFWhMc=
|
||||
github.com/crewjam/saml v0.4.13/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA=
|
||||
github.com/cs3org/reva/v2 v2.12.1-0.20230417084429-b3d96f9db80c h1:H6OjKTaRowZfAU/Hwvv4W0pLFFH/KNbHaNVNw3ANoHU=
|
||||
github.com/cs3org/reva/v2 v2.12.1-0.20230417084429-b3d96f9db80c/go.mod h1:FNAYs5H3xs8v0OFmNgZtiMAzIMXd/6TJmO0uZuNn8pQ=
|
||||
github.com/cs3org/reva/v2 v2.12.1-0.20230331184913-eca8953fb6e9 h1:zUMD0UvKVPbR3UaodnA0GXuBycxrIuaO8Kshgi3iKKI=
|
||||
github.com/cs3org/reva/v2 v2.12.1-0.20230331184913-eca8953fb6e9/go.mod h1:FNAYs5H3xs8v0OFmNgZtiMAzIMXd/6TJmO0uZuNn8pQ=
|
||||
github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 h1:Z9lwXumT5ACSmJ7WGnFl+OMLLjpz5uR2fyz7dC255FI=
|
||||
github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8/go.mod h1:4abs/jPXcmJzYoYGF91JF9Uq9s/KL5n1jvFDix8KcqY=
|
||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
gOidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
goidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
@@ -24,7 +24,7 @@ func newOidcOptions(opts ...Option) Options {
|
||||
|
||||
// OIDCProvider used to mock the oidc provider during tests
|
||||
type OIDCProvider interface {
|
||||
UserInfo(ctx context.Context, ts oauth2.TokenSource) (*gOidc.UserInfo, error)
|
||||
UserInfo(ctx context.Context, ts oauth2.TokenSource) (*goidc.UserInfo, error)
|
||||
}
|
||||
|
||||
// OidcAuth provides a middleware to authenticate a bearer auth with an OpenID Connect identity provider
|
||||
@@ -38,7 +38,7 @@ func OidcAuth(opts ...Option) func(http.Handler) http.Handler {
|
||||
// Initialize a provider by specifying the issuer URL.
|
||||
// it will fetch the keys from the issuer using the .well-known
|
||||
// endpoint
|
||||
return gOidc.NewProvider(
|
||||
return goidc.NewProvider(
|
||||
context.WithValue(context.Background(), oauth2.HTTPClient, http.Client{}),
|
||||
opt.OidcIssuer,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/MicahParks/keyfunc"
|
||||
goidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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) (jwt.RegisteredClaims, []string, 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 oidcClient struct {
|
||||
// Logger to use for logging, must be set
|
||||
Logger log.Logger
|
||||
|
||||
clientID string
|
||||
skipClientIDCheck bool
|
||||
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 ...
|
||||
providerLock: &sync.Mutex{},
|
||||
jwksLock: &sync.Mutex{},
|
||||
clientID: options.ClientID,
|
||||
skipClientIDCheck: options.SkipClientIDCheck,
|
||||
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 interface{}) 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) (jwt.RegisteredClaims, []string, error) {
|
||||
var mapClaims []string
|
||||
if err := c.lookupWellKnownOpenidConfiguration(ctx); err != nil {
|
||||
return jwt.RegisteredClaims{}, mapClaims, err
|
||||
}
|
||||
switch c.accessTokenVerifyMethod {
|
||||
case config.AccessTokenVerificationJWT:
|
||||
return c.verifyAccessTokenJWT(token)
|
||||
case config.AccessTokenVerificationNone:
|
||||
c.Logger.Debug().Msg("Access Token verification disabled")
|
||||
return jwt.RegisteredClaims{}, mapClaims, nil
|
||||
default:
|
||||
c.Logger.Error().Str("access_token_verify_method", c.accessTokenVerifyMethod).Msg("Unknown Access Token verification setting")
|
||||
return jwt.RegisteredClaims{}, 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) (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")
|
||||
}
|
||||
|
||||
_, err := jwt.ParseWithClaims(token, &claims, jwks.Keyfunc)
|
||||
if err != nil {
|
||||
return claims, mapClaims, err
|
||||
}
|
||||
_, mapClaims, err = new(jwt.Parser).ParseUnverified(token, jwt.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
|
||||
}
|
||||
|
||||
if !claims.VerifyIssuer(c.issuer, true) {
|
||||
vErr := jwt.ValidationError{}
|
||||
vErr.Inner = jwt.ErrTokenInvalidIssuer
|
||||
vErr.Errors |= jwt.ValidationErrorIssuer
|
||||
return claims, mapClaims, vErr
|
||||
}
|
||||
|
||||
return claims, mapClaims, nil
|
||||
}
|
||||
|
||||
func (c *oidcClient) VerifyLogoutToken(ctx context.Context, rawToken string) (*LogoutToken, error) {
|
||||
if err := c.lookupWellKnownOpenidConfiguration(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jws, err := jose.ParseSigned(rawToken)
|
||||
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(rawToken)
|
||||
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.
|
||||
var n struct {
|
||||
Nonce *string `json:"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 !c.skipIssuerValidation && token.Issuer != c.issuer {
|
||||
return nil, fmt.Errorf("oidc: logout token issued by a different provider, expected %q got %q", c.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 !c.skipClientIDCheck {
|
||||
if c.clientID != "" {
|
||||
if !contains(token.Audience, c.clientID) {
|
||||
return nil, fmt.Errorf("oidc: expected audience %q got %q", c.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: logout token not signed")
|
||||
case 1:
|
||||
// do nothing
|
||||
default:
|
||||
return nil, fmt.Errorf("oidc: multiple signatures on logout token not supported")
|
||||
}
|
||||
|
||||
sig := jws.Signatures[0]
|
||||
supportedSigAlgs := c.algorithms
|
||||
if len(supportedSigAlgs) == 0 {
|
||||
supportedSigAlgs = []string{RS256}
|
||||
}
|
||||
|
||||
if !contains(supportedSigAlgs, sig.Header.Algorithm) {
|
||||
return nil, fmt.Errorf("oidc: logout token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm)
|
||||
}
|
||||
|
||||
gotPayload, err := c.remoteKeySet.VerifySignature(goidc.ClientContext(ctx, c.httpClient), rawToken)
|
||||
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
|
||||
}
|
||||
|
||||
func unmarshalResp(r *http.Response, body []byte, v interface{}) 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)
|
||||
}
|
||||
|
||||
func contains(sli []string, ele string) bool {
|
||||
for _, s := range sli {
|
||||
if s == ele {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseJWT(p string) ([]byte, error) {
|
||||
parts := strings.Split(p, ".")
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts))
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package oidc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
goidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
type signingKey struct {
|
||||
keyID string // optional
|
||||
priv interface{}
|
||||
pub interface{}
|
||||
alg jose.SignatureAlgorithm
|
||||
}
|
||||
|
||||
// sign creates a JWS using the private key from the provided payload.
|
||||
func (s *signingKey) sign(t testing.TB, payload []byte) string {
|
||||
privKey := &jose.JSONWebKey{Key: s.priv, Algorithm: string(s.alg), KeyID: s.keyID}
|
||||
|
||||
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: s.alg, Key: privKey}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jws, err := signer.Sign(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := jws.CompactSerialize()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *signingKey) jwk() jose.JSONWebKey {
|
||||
return jose.JSONWebKey{Key: s.pub, Use: "sig", Algorithm: string(s.alg), KeyID: s.keyID}
|
||||
}
|
||||
|
||||
func TestLogoutVerify(t *testing.T) {
|
||||
tests := []logoutVerificationTest{
|
||||
{
|
||||
name: "good token",
|
||||
logoutToken: ` {
|
||||
"iss": "https://foo",
|
||||
"sub": "248289761001",
|
||||
"aud": "s6BhdRkqt3",
|
||||
"iat": 1471566154,
|
||||
"jti": "bWJq",
|
||||
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
|
||||
"events": {
|
||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
||||
}
|
||||
}`,
|
||||
config: goidc.Config{
|
||||
SkipClientIDCheck: true,
|
||||
},
|
||||
signKey: newRSAKey(t),
|
||||
},
|
||||
{
|
||||
name: "invalid issuer",
|
||||
issuer: "https://bar",
|
||||
logoutToken: `{"iss":"https://foo"}`,
|
||||
config: goidc.Config{
|
||||
SkipClientIDCheck: true,
|
||||
SkipExpiryCheck: true,
|
||||
},
|
||||
signKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid sig",
|
||||
logoutToken: `{
|
||||
"iss": "https://foo",
|
||||
"sub": "248289761001",
|
||||
"aud": "s6BhdRkqt3",
|
||||
"iat": 1471566154,
|
||||
"jti": "bWJq",
|
||||
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
|
||||
"events": {
|
||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
||||
}
|
||||
}`,
|
||||
config: goidc.Config{
|
||||
SkipClientIDCheck: true,
|
||||
SkipExpiryCheck: true,
|
||||
},
|
||||
signKey: newRSAKey(t),
|
||||
verificationKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no sid and no sub",
|
||||
logoutToken: ` {
|
||||
"iss": "https://foo",
|
||||
"aud": "s6BhdRkqt3",
|
||||
"iat": 1471566154,
|
||||
"jti": "bWJq",
|
||||
"events": {
|
||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
||||
}
|
||||
}`,
|
||||
config: goidc.Config{
|
||||
SkipClientIDCheck: true,
|
||||
},
|
||||
signKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Prohibited nonce present",
|
||||
logoutToken: ` {
|
||||
"iss": "https://foo",
|
||||
"sub": "248289761001",
|
||||
"aud": "s6BhdRkqt3",
|
||||
"iat": 1471566154,
|
||||
"jti": "bWJq",
|
||||
"nonce" : "prohibited",
|
||||
"events": {
|
||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
||||
}
|
||||
}`,
|
||||
config: goidc.Config{
|
||||
SkipClientIDCheck: true,
|
||||
},
|
||||
signKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Wrong Event string",
|
||||
logoutToken: ` {
|
||||
"iss": "https://foo",
|
||||
"sub": "248289761001",
|
||||
"aud": "s6BhdRkqt3",
|
||||
"iat": 1471566154,
|
||||
"jti": "bWJq",
|
||||
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
|
||||
"events": {
|
||||
"not a logout event": {}
|
||||
}
|
||||
}`,
|
||||
config: goidc.Config{
|
||||
SkipClientIDCheck: true,
|
||||
},
|
||||
signKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "No Event string",
|
||||
logoutToken: ` {
|
||||
"iss": "https://foo",
|
||||
"sub": "248289761001",
|
||||
"aud": "s6BhdRkqt3",
|
||||
"iat": 1471566154,
|
||||
"jti": "bWJq",
|
||||
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
|
||||
}`,
|
||||
config: goidc.Config{
|
||||
SkipClientIDCheck: true,
|
||||
},
|
||||
signKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyAudienceLogout(t *testing.T) {
|
||||
tests := []logoutVerificationTest{
|
||||
{
|
||||
name: "good audience",
|
||||
logoutToken: `{"iss":"https://foo","aud":"client1","sub":"subject","events": {
|
||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
||||
}
|
||||
}`,
|
||||
config: goidc.Config{
|
||||
ClientID: "client1",
|
||||
SkipExpiryCheck: true,
|
||||
},
|
||||
signKey: newRSAKey(t),
|
||||
},
|
||||
{
|
||||
name: "mismatched audience",
|
||||
logoutToken: `{"iss":"https://foo","aud":"client2","sub":"subject","events": {
|
||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
||||
}}`,
|
||||
config: goidc.Config{
|
||||
ClientID: "client1",
|
||||
SkipExpiryCheck: true,
|
||||
},
|
||||
signKey: newRSAKey(t),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "multiple audiences, one matches",
|
||||
logoutToken: `{"iss":"https://foo","aud":["client1","client2"],"sub":"subject","events": {
|
||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
||||
}}`,
|
||||
config: goidc.Config{
|
||||
ClientID: "client2",
|
||||
SkipExpiryCheck: true,
|
||||
},
|
||||
signKey: newRSAKey(t),
|
||||
},
|
||||
}
|
||||
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 string
|
||||
|
||||
// Key to sign the ID Token with.
|
||||
signKey *signingKey
|
||||
// If not provided defaults to signKey. Only useful when
|
||||
// testing invalid signatures.
|
||||
verificationKey *signingKey
|
||||
|
||||
config goidc.Config
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
type testVerifier struct {
|
||||
jwk jose.JSONWebKey
|
||||
}
|
||||
|
||||
func (t *testVerifier) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
|
||||
jws, err := jose.ParseSigned(jwt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
|
||||
}
|
||||
return jws.Verify(&t.jwk)
|
||||
}
|
||||
|
||||
func (v logoutVerificationTest) runGetToken(t *testing.T) (*oidc.LogoutToken, error) {
|
||||
token := v.signKey.sign(t, []byte(v.logoutToken))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
issuer := "https://foo"
|
||||
var ks goidc.KeySet
|
||||
if v.verificationKey == nil {
|
||||
ks = &testVerifier{v.signKey.jwk()}
|
||||
} else {
|
||||
ks = &testVerifier{v.verificationKey.jwk()}
|
||||
}
|
||||
|
||||
pm := oidc.ProviderMetadata{}
|
||||
var clientID string
|
||||
switch t.Name() {
|
||||
case "TestLogoutVerify/good_token":
|
||||
clientID = "s6BhdRkqt3"
|
||||
default:
|
||||
clientID = "client1"
|
||||
}
|
||||
verifier := oidc.NewOIDCClient(
|
||||
oidc.WithOidcIssuer(issuer),
|
||||
oidc.WithKeySet(ks),
|
||||
oidc.WithConfig(&v.config),
|
||||
oidc.WithProviderMetadata(&pm),
|
||||
oidc.WithClientID(clientID),
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
return &signingKey{"", priv, priv.Public(), jose.RS256}
|
||||
}
|
||||
|
||||
func newECDSAKey(t *testing.T) *signingKey {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &signingKey{"", priv, priv.Public(), jose.ES256}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
@@ -53,6 +54,44 @@ type ProviderMetadata struct {
|
||||
//claim_types_supported
|
||||
}
|
||||
|
||||
// Logout Token defines an 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 jwt.ClaimStrings `json:"aud"` // "s6BhdRkqt3"
|
||||
|
||||
// When the token was issued by the provider.
|
||||
IssuedAt *jwt.NumericDate `json:"iat"`
|
||||
|
||||
// The Session Id
|
||||
SessionId string `json:"sid"`
|
||||
|
||||
Events LogoutEvent `json:"events"`
|
||||
|
||||
// Jwt Id
|
||||
JwtID string `json:"jti"`
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
oauth2 "golang.org/x/oauth2"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
)
|
||||
|
||||
// OIDCProvider is an autogenerated mock type for the OIDCProvider type
|
||||
type OIDCProvider struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// UserInfo provides a mock function with given fields: ctx, ts
|
||||
func (_m *OIDCProvider) UserInfo(ctx context.Context, ts oauth2.TokenSource) (*oidc.UserInfo, error) {
|
||||
ret := _m.Called(ctx, ts)
|
||||
|
||||
var r0 *oidc.UserInfo
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, oauth2.TokenSource) (*oidc.UserInfo, error)); ok {
|
||||
return rf(ctx, ts)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, oauth2.TokenSource) *oidc.UserInfo); ok {
|
||||
r0 = rf(ctx, ts)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*oidc.UserInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, oauth2.TokenSource) error); ok {
|
||||
r1 = rf(ctx, ts)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewOIDCProvider interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewOIDCProvider creates a new instance of OIDCProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewOIDCProvider(t mockConstructorTestingTNewOIDCProvider) *OIDCProvider {
|
||||
mock := &OIDCProvider{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/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
|
||||
// KeySet to use when verifiing signatures
|
||||
KeySet KeySet
|
||||
// AccessTokenVerifyMethod to use when verifying access tokens
|
||||
// TODO pass a function or interface to verify? an AccessTokenVerifier?
|
||||
AccessTokenVerifyMethod string
|
||||
// ClientID the client id to expect in tokens. If not set SkipClientIDCheck must be true
|
||||
// TODO also check in access token
|
||||
ClientID string
|
||||
// SkipClientIDCheck must be true if ClientID is empty
|
||||
SkipClientIDCheck bool
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// WithKeySet provides a function to set the KeySet option.
|
||||
func WithKeySet(val KeySet) Option {
|
||||
return func(o *Options) {
|
||||
o.KeySet = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithClientID provides a function to set the clientID option.
|
||||
func WithClientID(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.ClientID = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithSkipClientIDCheck provides a function to set the skipClientIDCheck option.
|
||||
func WithSkipClientIDCheck(val bool) Option {
|
||||
return func(o *Options) {
|
||||
o.SkipClientIDCheck = 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
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,22 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/justinas/alice"
|
||||
"github.com/oklog/run"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
pkgmiddleware "github.com/owncloud/ocis/v2/ocis-pkg/middleware"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/store"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
@@ -36,7 +39,6 @@ import (
|
||||
"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
|
||||
"github.com/urfave/cli/v2"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Server is the entrypoint for the server command.
|
||||
@@ -49,6 +51,15 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
return configlog.ReturnFatal(parser.ParseConfig(cfg))
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
userInfoCache := store.Create(
|
||||
store.Store(cfg.OIDC.UserinfoCache.Store),
|
||||
store.TTL(cfg.OIDC.UserinfoCache.TTL),
|
||||
store.Size(cfg.OIDC.UserinfoCache.Size),
|
||||
microstore.Nodes(cfg.OIDC.UserinfoCache.Nodes...),
|
||||
microstore.Database(cfg.OIDC.UserinfoCache.Database),
|
||||
microstore.Table(cfg.OIDC.UserinfoCache.Table),
|
||||
)
|
||||
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
err := tracing.Configure(cfg)
|
||||
if err != nil {
|
||||
@@ -59,6 +70,27 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
var oidcHTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: cfg.OIDC.Insecure, //nolint:gosec
|
||||
},
|
||||
DisableKeepAlives: true,
|
||||
},
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
|
||||
oidcClient := oidc.NewOIDCClient(
|
||||
oidc.WithAccessTokenVerifyMethod(cfg.OIDC.AccessTokenVerifyMethod),
|
||||
oidc.WithLogger(logger),
|
||||
oidc.WithHTTPClient(oidcHTTPClient),
|
||||
oidc.WithOidcIssuer(cfg.OIDC.Issuer),
|
||||
oidc.WithJWKSOptions(cfg.OIDC.JWKS),
|
||||
oidc.WithClientID(cfg.OIDC.ClientID),
|
||||
oidc.WithSkipClientIDCheck(cfg.OIDC.SkipClientIDCheck),
|
||||
)
|
||||
|
||||
var (
|
||||
m = metrics.New()
|
||||
)
|
||||
@@ -79,18 +111,28 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
proxy.Logger(logger),
|
||||
proxy.Config(cfg),
|
||||
)
|
||||
|
||||
lh := StaticRouteHandler{
|
||||
prefix: cfg.HTTP.Root,
|
||||
userInfoCache: userInfoCache,
|
||||
logger: logger,
|
||||
config: *cfg,
|
||||
oidcClient: oidcClient,
|
||||
proxy: rp,
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to initialize reverse proxy: %w", err)
|
||||
return fmt.Errorf("failed to initialize reverse proxy: %w", err)
|
||||
}
|
||||
|
||||
{
|
||||
middlewares := loadMiddlewares(ctx, logger, cfg, userInfoCache)
|
||||
server, err := proxyHTTP.Server(
|
||||
proxyHTTP.Handler(rp),
|
||||
proxyHTTP.Handler(lh.handler()),
|
||||
proxyHTTP.Logger(logger),
|
||||
proxyHTTP.Context(ctx),
|
||||
proxyHTTP.Config(cfg),
|
||||
proxyHTTP.Metrics(metrics.New()),
|
||||
proxyHTTP.Middlewares(loadMiddlewares(ctx, logger, cfg)),
|
||||
proxyHTTP.Middlewares(middlewares),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -137,7 +179,86 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) alice.Chain {
|
||||
// StaticRouteHandler defines a Route Handler for static routes
|
||||
type StaticRouteHandler struct {
|
||||
prefix string
|
||||
proxy http.Handler
|
||||
userInfoCache microstore.Store
|
||||
logger log.Logger
|
||||
config config.Config
|
||||
oidcClient oidc.OIDCClient
|
||||
}
|
||||
|
||||
func (h *StaticRouteHandler) handler() http.Handler {
|
||||
m := chi.NewMux()
|
||||
m.Route(h.prefix, func(r chi.Router) {
|
||||
// Wrapper for backchannel logout
|
||||
r.Post("/backchannel_logout", h.backchannelLogout)
|
||||
|
||||
// TODO: migrate oidc well knowns here in a second wrapper
|
||||
r.HandleFunc("/*", h.proxy.ServeHTTP)
|
||||
|
||||
})
|
||||
// This is commented out due to a race issue in chi
|
||||
//var methods = []string{"PROPFIND", "DELETE", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "REPORT"}
|
||||
//for _, k := range methods {
|
||||
// chi.RegisterMethod(k)
|
||||
//}
|
||||
|
||||
// To avoid using the chi.RegisterMethod() this is basically a catchAll for all HTTP Methods that are not
|
||||
// covered in chi by default
|
||||
m.MethodNotAllowed(h.proxy.ServeHTTP)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
type jse struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
// handle backchannel logout requests as per https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRequest
|
||||
func (h *StaticRouteHandler) backchannelLogout(w http.ResponseWriter, r *http.Request) {
|
||||
// parse the application/x-www-form-urlencoded POST request
|
||||
if err := r.ParseForm(); err != nil {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
logoutToken, err := h.oidcClient.VerifyLogoutToken(r.Context(), r.PostFormValue("logout_token"))
|
||||
if err != nil {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
records, err := h.userInfoCache.Read(logoutToken.SessionId)
|
||||
if errors.Is(err, microstore.ErrNotFound) || len(records) == 0 {
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, nil)
|
||||
return
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
err = h.userInfoCache.Delete(string(record.Value))
|
||||
if !errors.Is(err, microstore.ErrNotFound) {
|
||||
// Spec requires us to return a 400 BadRequest when the session could not be destroyed
|
||||
h.logger.Err(err).Msg("could not delete user info from cache")
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, jse{Error: "invalid_request", ErrorDescription: err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// we can ignore errors when cleaning up the lookup table
|
||||
_ = h.userInfoCache.Delete(logoutToken.SessionId)
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, nil)
|
||||
}
|
||||
|
||||
func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config, userInfoCache microstore.Store) alice.Chain {
|
||||
rolesClient := settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient())
|
||||
revaClient, err := pool.GetGatewayServiceClient(cfg.Reva.Address, cfg.Reva.GetRevaOptions()...)
|
||||
if err != nil {
|
||||
@@ -212,32 +333,19 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
|
||||
})
|
||||
}
|
||||
|
||||
cache := store.Create(
|
||||
store.Store(cfg.OIDC.UserinfoCache.Store),
|
||||
store.TTL(cfg.OIDC.UserinfoCache.TTL),
|
||||
store.Size(cfg.OIDC.UserinfoCache.Size),
|
||||
microstore.Nodes(cfg.OIDC.UserinfoCache.Nodes...),
|
||||
microstore.Database(cfg.OIDC.UserinfoCache.Database),
|
||||
microstore.Table(cfg.OIDC.UserinfoCache.Table),
|
||||
)
|
||||
|
||||
authenticators = append(authenticators, middleware.NewOIDCAuthenticator(
|
||||
middleware.Logger(logger),
|
||||
middleware.Cache(cache),
|
||||
middleware.UserInfoCache(userInfoCache),
|
||||
middleware.DefaultAccessTokenTTL(cfg.OIDC.UserinfoCache.TTL),
|
||||
middleware.HTTPClient(oidcHTTPClient),
|
||||
middleware.OIDCIss(cfg.OIDC.Issuer),
|
||||
middleware.JWKSOptions(cfg.OIDC.JWKS),
|
||||
middleware.AccessTokenVerifyMethod(cfg.OIDC.AccessTokenVerifyMethod),
|
||||
middleware.OIDCProviderFunc(func() (middleware.OIDCProvider, error) {
|
||||
// Initialize a provider by specifying the issuer URL.
|
||||
// it will fetch the keys from the issuer using the .well-known
|
||||
// endpoint
|
||||
return oidc.NewProvider(
|
||||
context.WithValue(ctx, oauth2.HTTPClient, oidcHTTPClient),
|
||||
cfg.OIDC.Issuer,
|
||||
)
|
||||
}),
|
||||
middleware.OIDCClient(oidc.NewOIDCClient(
|
||||
oidc.WithAccessTokenVerifyMethod(cfg.OIDC.AccessTokenVerifyMethod),
|
||||
oidc.WithLogger(logger),
|
||||
oidc.WithHTTPClient(oidcHTTPClient),
|
||||
oidc.WithOidcIssuer(cfg.OIDC.Issuer),
|
||||
oidc.WithJWKSOptions(cfg.OIDC.JWKS),
|
||||
)),
|
||||
))
|
||||
authenticators = append(authenticators, middleware.PublicShareAuthenticator{
|
||||
Logger: logger,
|
||||
|
||||
@@ -108,6 +108,8 @@ type OIDC struct {
|
||||
UserinfoCache *Cache `yaml:"user_info_cache"`
|
||||
JWKS JWKS `yaml:"jwks"`
|
||||
RewriteWellKnown bool `yaml:"rewrite_well_known" env:"PROXY_OIDC_REWRITE_WELLKNOWN" desc:"Enables rewriting the /.well-known/openid-configuration to the configured OIDC issuer. Needed by the Desktop Client, Android Client and iOS Client to discover the OIDC provider."`
|
||||
ClientID string `yaml:"client_id" env:"OCIS_OIDC_CLIENT_ID;PROXY_OIDC_CLIENT_ID" desc:"OIDC client ID, which ownCloud Web uses. This client needs to be set up in your external IDP (has no effect when using the builtin IDP)."`
|
||||
SkipClientIDCheck bool `yaml:"skip_client_id_check" env:"PROXY_OIDC_SKIP_CLIENT_ID_CHECK" desc:"If true will skip checking the configured client ID is present in audience claims. See following chapter for more details: https://openid.net/specs/openid-connect-core-1_0.html#IDToken"`
|
||||
}
|
||||
|
||||
type JWKS struct {
|
||||
|
||||
@@ -53,6 +53,7 @@ func DefaultConfig() *config.Config {
|
||||
RefreshTimeout: 10, // seconds
|
||||
RefreshUnknownKID: true,
|
||||
},
|
||||
ClientID: "web",
|
||||
},
|
||||
PolicySelector: nil,
|
||||
RoleAssignment: config.RoleAssignment{
|
||||
|
||||
@@ -3,17 +3,14 @@ package middleware
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
|
||||
"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
|
||||
|
||||
"github.com/MicahParks/keyfunc"
|
||||
gOidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/shamaton/msgpack/v2"
|
||||
@@ -27,26 +24,18 @@ const (
|
||||
_bearerPrefix = "Bearer "
|
||||
)
|
||||
|
||||
// OIDCProvider used to mock the oidc provider during tests
|
||||
type OIDCProvider interface {
|
||||
UserInfo(ctx context.Context, ts oauth2.TokenSource) (*gOidc.UserInfo, error)
|
||||
}
|
||||
|
||||
// NewOIDCAuthenticator returns a ready to use authenticator which can handle OIDC authentication.
|
||||
func NewOIDCAuthenticator(opts ...Option) *OIDCAuthenticator {
|
||||
options := newOptions(opts...)
|
||||
|
||||
return &OIDCAuthenticator{
|
||||
Logger: options.Logger,
|
||||
userInfoCache: options.Cache,
|
||||
userInfoCache: options.UserInfoCache,
|
||||
DefaultTokenCacheTTL: options.DefaultAccessTokenTTL,
|
||||
HTTPClient: options.HTTPClient,
|
||||
OIDCIss: options.OIDCIss,
|
||||
ProviderFunc: options.OIDCProviderFunc,
|
||||
JWKSOptions: options.JWKS,
|
||||
oidcClient: options.OIDCClient,
|
||||
AccessTokenVerifyMethod: options.AccessTokenVerifyMethod,
|
||||
providerLock: &sync.Mutex{},
|
||||
jwksLock: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,21 +46,14 @@ type OIDCAuthenticator struct {
|
||||
OIDCIss string
|
||||
userInfoCache store.Store
|
||||
DefaultTokenCacheTTL time.Duration
|
||||
ProviderFunc func() (OIDCProvider, error)
|
||||
oidcClient oidc.OIDCClient
|
||||
AccessTokenVerifyMethod string
|
||||
JWKSOptions config.JWKS
|
||||
|
||||
providerLock *sync.Mutex
|
||||
provider OIDCProvider
|
||||
|
||||
jwksLock *sync.Mutex
|
||||
JWKS *keyfunc.JWKS
|
||||
}
|
||||
|
||||
func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[string]interface{}, error) {
|
||||
var claims map[string]interface{}
|
||||
|
||||
// usea 64 bytes long hash to have 256-bit collision resistance.
|
||||
// use a 64 bytes long hash to have 256-bit collision resistance.
|
||||
hash := make([]byte, 64)
|
||||
sha3.ShakeSum256(hash, []byte(token))
|
||||
encodedHash := base64.URLEncoding.EncodeToString(hash)
|
||||
@@ -87,7 +69,8 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
|
||||
}
|
||||
m.Logger.Error().Err(err).Msg("could not unmarshal userinfo")
|
||||
}
|
||||
aClaims, err := m.verifyAccessToken(token)
|
||||
|
||||
aClaims, _, err := m.oidcClient.VerifyAccessToken(req.Context(), token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to verify access token")
|
||||
}
|
||||
@@ -96,7 +79,7 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
|
||||
AccessToken: token,
|
||||
}
|
||||
|
||||
userInfo, err := m.getProvider().UserInfo(
|
||||
userInfo, err := m.oidcClient.UserInfo(
|
||||
context.WithValue(req.Context(), oauth2.HTTPClient, m.HTTPClient),
|
||||
oauth2.StaticTokenSource(oauth2Token),
|
||||
)
|
||||
@@ -120,6 +103,18 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
|
||||
if err != nil {
|
||||
m.Logger.Error().Err(err).Msg("failed to write to userinfo cache")
|
||||
}
|
||||
|
||||
if sid, ok := claims["sid"]; ok {
|
||||
// reuse user cache for session id lookup
|
||||
err = m.userInfoCache.Write(&store.Record{
|
||||
Key: fmt.Sprintf("%s", sid),
|
||||
Value: []byte(encodedHash),
|
||||
Expiry: time.Until(expiration),
|
||||
})
|
||||
if err != nil {
|
||||
m.Logger.Error().Err(err).Msg("failed to write session lookup cache")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -127,44 +122,6 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (m OIDCAuthenticator) verifyAccessToken(token string) (jwt.RegisteredClaims, error) {
|
||||
switch m.AccessTokenVerifyMethod {
|
||||
case config.AccessTokenVerificationJWT:
|
||||
return m.verifyAccessTokenJWT(token)
|
||||
case config.AccessTokenVerificationNone:
|
||||
m.Logger.Debug().Msg("Access Token verification disabled")
|
||||
return jwt.RegisteredClaims{}, nil
|
||||
default:
|
||||
m.Logger.Error().Str("access_token_verify_method", m.AccessTokenVerifyMethod).Msg("Unknown Access Token verification setting")
|
||||
return jwt.RegisteredClaims{}, errors.New("Unknown Access Token Verification method")
|
||||
}
|
||||
}
|
||||
|
||||
// verifyAccessTokenJWT tries to parse and verify the access token as a JWT.
|
||||
func (m OIDCAuthenticator) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, error) {
|
||||
var claims jwt.RegisteredClaims
|
||||
jwks := m.getKeyfunc()
|
||||
if jwks == nil {
|
||||
return claims, errors.New("Error initializing jwks keyfunc")
|
||||
}
|
||||
|
||||
_, err := jwt.ParseWithClaims(token, &claims, jwks.Keyfunc)
|
||||
m.Logger.Debug().Interface("access token", &claims).Msg("parsed access token")
|
||||
if err != nil {
|
||||
m.Logger.Info().Err(err).Msg("Failed to parse/verify the access token.")
|
||||
return claims, err
|
||||
}
|
||||
|
||||
if !claims.VerifyIssuer(m.OIDCIss, true) {
|
||||
vErr := jwt.ValidationError{}
|
||||
vErr.Inner = jwt.ErrTokenInvalidIssuer
|
||||
vErr.Errors |= jwt.ValidationErrorIssuer
|
||||
return claims, vErr
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// extractExpiration tries to extract the expriration time from the access token
|
||||
// If the access token does not have an exp claim it will fallback to the configured
|
||||
// default expiration
|
||||
@@ -186,56 +143,6 @@ func (m OIDCAuthenticator) shouldServe(req *http.Request) bool {
|
||||
return strings.HasPrefix(header, _bearerPrefix)
|
||||
}
|
||||
|
||||
func (m *OIDCAuthenticator) getKeyfunc() *keyfunc.JWKS {
|
||||
m.jwksLock.Lock()
|
||||
defer m.jwksLock.Unlock()
|
||||
if m.JWKS == nil {
|
||||
oidcMetadata, err := oidc.GetIDPMetadata(m.Logger, m.HTTPClient, m.OIDCIss)
|
||||
if err != nil {
|
||||
m.Logger.Error().Err(err).Msg("failed to decode provider openid-configuration")
|
||||
return nil
|
||||
}
|
||||
m.Logger.Debug().Str("jwks", oidcMetadata.JwksURI).Msg("discovered jwks endpoint")
|
||||
options := keyfunc.Options{
|
||||
Client: m.HTTPClient,
|
||||
RefreshErrorHandler: func(err error) {
|
||||
m.Logger.Error().Err(err).Msg("There was an error with the jwt.Keyfunc")
|
||||
},
|
||||
RefreshInterval: time.Minute * time.Duration(m.JWKSOptions.RefreshInterval),
|
||||
RefreshRateLimit: time.Second * time.Duration(m.JWKSOptions.RefreshRateLimit),
|
||||
RefreshTimeout: time.Second * time.Duration(m.JWKSOptions.RefreshTimeout),
|
||||
RefreshUnknownKID: m.JWKSOptions.RefreshUnknownKID,
|
||||
}
|
||||
m.JWKS, err = keyfunc.Get(oidcMetadata.JwksURI, options)
|
||||
if err != nil {
|
||||
m.JWKS = nil
|
||||
m.Logger.Error().Err(err).Msg("Failed to create JWKS from resource at the given URL.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return m.JWKS
|
||||
}
|
||||
|
||||
func (m *OIDCAuthenticator) getProvider() OIDCProvider {
|
||||
m.providerLock.Lock()
|
||||
defer m.providerLock.Unlock()
|
||||
if m.provider == nil {
|
||||
// Lazily initialize a provider
|
||||
|
||||
// provider needs to be cached as when it is created
|
||||
// it will fetch the keys from the issuer using the .well-known
|
||||
// endpoint
|
||||
provider, err := m.ProviderFunc()
|
||||
if err != nil {
|
||||
m.Logger.Error().Err(err).Msg("could not initialize oidcAuth provider")
|
||||
return nil
|
||||
}
|
||||
|
||||
m.provider = provider
|
||||
}
|
||||
return m.provider
|
||||
}
|
||||
|
||||
// Authenticate implements the authenticator interface to authenticate requests via oidc auth.
|
||||
func (m *OIDCAuthenticator) Authenticate(r *http.Request) (*http.Request, bool) {
|
||||
// there is no bearer token on the request,
|
||||
@@ -245,15 +152,6 @@ func (m *OIDCAuthenticator) Authenticate(r *http.Request) (*http.Request, bool)
|
||||
// implement an early return here for paths we can't authenticate in this authenticator.
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if m.getProvider() == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Force init of jwks keyfunc if needed (contacts the .well-known and jwks endpoints on first call)
|
||||
if m.AccessTokenVerifyMethod == config.AccessTokenVerificationJWT && m.getKeyfunc() == nil {
|
||||
return nil, false
|
||||
}
|
||||
token := strings.TrimPrefix(r.Header.Get(_headerAuthorization), _bearerPrefix)
|
||||
|
||||
claims, err := m.getClaims(token, r)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
|
||||
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
|
||||
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
|
||||
"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
|
||||
@@ -34,7 +35,7 @@ type Options struct {
|
||||
// SettingsRoleService for the roles API in settings
|
||||
SettingsRoleService settingssvc.RoleService
|
||||
// OIDCProviderFunc to lazily initialize an oidc provider, must be set for the oidc_auth middleware
|
||||
OIDCProviderFunc func() (OIDCProvider, error)
|
||||
OIDCClient oidc.OIDCClient
|
||||
// OIDCIss is the oidcAuth-issuer
|
||||
OIDCIss string
|
||||
// RevaGatewayClient to send requests to the reva gateway
|
||||
@@ -53,8 +54,8 @@ type Options struct {
|
||||
EnableBasicAuth bool
|
||||
// DefaultAccessTokenTTL is used to calculate the expiration when an access token has no expiration set
|
||||
DefaultAccessTokenTTL time.Duration
|
||||
// Cache sets the access token cache store
|
||||
Cache store.Store
|
||||
// UserInfoCache sets the access token cache store
|
||||
UserInfoCache store.Store
|
||||
// CredentialsByUserAgent sets the auth challenges on a per user-agent basis
|
||||
CredentialsByUserAgent map[string]string
|
||||
// AccessTokenVerifyMethod configures how access_tokens should be verified but the oidc_auth middleware.
|
||||
@@ -113,10 +114,10 @@ func SettingsRoleService(rc settingssvc.RoleService) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// OIDCProviderFunc provides a function to set the the oidc provider function option.
|
||||
func OIDCProviderFunc(f func() (OIDCProvider, error)) Option {
|
||||
// OIDCClient provides a function to set the the oidc client option.
|
||||
func OIDCClient(val oidc.OIDCClient) Option {
|
||||
return func(o *Options) {
|
||||
o.OIDCProviderFunc = f
|
||||
o.OIDCClient = val
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,10 +191,10 @@ func DefaultAccessTokenTTL(ttl time.Duration) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// Cache provides a function to set the Cache
|
||||
func Cache(val store.Store) Option {
|
||||
// UserInfoCache provides a function to set the UserInfoCache
|
||||
func UserInfoCache(val store.Store) Option {
|
||||
return func(o *Options) {
|
||||
o.Cache = val
|
||||
o.UserInfoCache = val
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,13 +219,6 @@ func AccessTokenVerifyMethod(method string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// JWKSOptions sets the options for fetching the JWKS from the IDP
|
||||
func JWKSOptions(jo config.JWKS) Option {
|
||||
return func(o *Options) {
|
||||
o.JWKS = jo
|
||||
}
|
||||
}
|
||||
|
||||
// RoleQuotas sets the role quota mapping setting
|
||||
func RoleQuotas(roleQuotas map[string]uint64) Option {
|
||||
return func(o *Options) {
|
||||
|
||||
@@ -67,7 +67,7 @@ type WebConfig struct {
|
||||
type OIDC struct {
|
||||
MetadataURL string `json:"metadata_url,omitempty" yaml:"metadata_url" env:"WEB_OIDC_METADATA_URL" desc:"URL for the OIDC well-known configuration endpoint. Defaults to the oCIS API URL + \"/.well-known/openid-configuration\"."`
|
||||
Authority string `json:"authority,omitempty" yaml:"authority" env:"OCIS_URL;OCIS_OIDC_ISSUER;WEB_OIDC_AUTHORITY" desc:"URL of the OIDC issuer. It defaults to URL of the builtin IDP."`
|
||||
ClientID string `json:"client_id,omitempty" yaml:"client_id" env:"WEB_OIDC_CLIENT_ID" desc:"OIDC client ID, which ownCloud Web uses. This client needs to be set up in your IDP."`
|
||||
ClientID string `json:"client_id,omitempty" yaml:"client_id" env:"OCIS_OIDC_CLIENT_ID;WEB_OIDC_CLIENT_ID" desc:"OIDC client ID, which ownCloud Web uses. This client needs to be set up in your IDP (has no effect when using the builtin IDP)."`
|
||||
ResponseType string `json:"response_type,omitempty" yaml:"response_type" env:"WEB_OIDC_RESPONSE_TYPE" desc:"OIDC response type to use for authentication."`
|
||||
Scope string `json:"scope,omitempty" yaml:"scope" env:"WEB_OIDC_SCOPE" desc:"OIDC scopes to request during authentication to authorize access to user details. Defaults to 'openid profile email'. Values are separated by blank. More example values but not limited to are 'address' or 'phone' etc."`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user