374 lines
15 KiB
Go
374 lines
15 KiB
Go
package oidc
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/rsa"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/go-crypt/crypt"
|
|
"github.com/go-crypt/crypt/algorithm"
|
|
"github.com/go-crypt/crypt/algorithm/plaintext"
|
|
"github.com/golang-jwt/jwt/v4"
|
|
"github.com/ory/fosite"
|
|
"github.com/ory/x/errorsx"
|
|
"github.com/pkg/errors"
|
|
"gopkg.in/square/go-jose.v2"
|
|
|
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
|
)
|
|
|
|
// NewHasher returns a new Hasher.
|
|
func NewHasher() (hasher *Hasher, err error) {
|
|
hasher = &Hasher{}
|
|
|
|
if hasher.decoder, err = crypt.NewDefaultDecoder(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = plaintext.RegisterDecoderPlainText(hasher.decoder); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return hasher, nil
|
|
}
|
|
|
|
// Hasher implements the fosite.Hasher interface and adaptively compares hashes.
|
|
type Hasher struct {
|
|
decoder algorithm.DecoderRegister
|
|
}
|
|
|
|
// Compare compares the hash with the data and returns an error if they don't match.
|
|
func (h Hasher) Compare(_ context.Context, hash, data []byte) (err error) {
|
|
var digest algorithm.Digest
|
|
|
|
if digest, err = h.decoder.Decode(string(hash)); err != nil {
|
|
return err
|
|
}
|
|
|
|
if digest.MatchBytes(data) {
|
|
return nil
|
|
}
|
|
|
|
return errPasswordsDoNotMatch
|
|
}
|
|
|
|
// Hash creates a new hash from data.
|
|
func (h Hasher) Hash(_ context.Context, data []byte) (hash []byte, err error) {
|
|
return data, nil
|
|
}
|
|
|
|
// DefaultClientAuthenticationStrategy is a copy of fosite's with the addition of the client_secret_jwt method and some
|
|
// minor superficial changes.
|
|
//
|
|
//nolint:gocyclo // Complexity is necessary to remain in feature parity.
|
|
func (p *OpenIDConnectProvider) DefaultClientAuthenticationStrategy(ctx context.Context, r *http.Request, form url.Values) (client fosite.Client, err error) {
|
|
if assertionType := form.Get(FormParameterClientAssertionType); assertionType == ClientAssertionJWTBearerType {
|
|
assertion := form.Get(FormParameterClientAssertion)
|
|
if len(assertion) == 0 {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("The client_assertion request parameter must be set when using client_assertion_type of '%s'.", ClientAssertionJWTBearerType))
|
|
}
|
|
|
|
var (
|
|
token *jwt.Token
|
|
clientID string
|
|
)
|
|
|
|
token, err = jwt.ParseWithClaims(assertion, jwt.MapClaims{}, func(t *jwt.Token) (any, error) {
|
|
clientID, _, err = clientCredentialsFromRequestBody(form, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if clientID == "" {
|
|
claims := t.Claims.(jwt.MapClaims)
|
|
|
|
if sub, ok := claims[ClaimSubject].(string); !ok {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("The claim 'sub' from the client_assertion JSON Web Token is undefined."))
|
|
} else {
|
|
clientID = sub
|
|
}
|
|
}
|
|
|
|
if client, err = p.Store.GetClient(ctx, clientID); err != nil {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithWrap(err).WithDebug(err.Error()))
|
|
}
|
|
|
|
oidcClient, ok := client.(*FullClient)
|
|
if !ok {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("The client configuration does not support OpenID Connect specific authentication methods."))
|
|
}
|
|
|
|
switch oidcClient.GetTokenEndpointAuthMethod() {
|
|
case ClientAuthMethodPrivateKeyJWT, ClientAuthMethodClientSecretJWT:
|
|
break
|
|
case ClientAuthMethodNone:
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("This requested OAuth 2.0 client does not support client authentication, however 'client_assertion' was provided in the request."))
|
|
case ClientAuthMethodClientSecretPost:
|
|
fallthrough
|
|
case ClientAuthMethodClientSecretBasic:
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("This requested OAuth 2.0 client only supports client authentication method '%s', however 'client_assertion' was provided in the request.", oidcClient.GetTokenEndpointAuthMethod()))
|
|
default:
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("This requested OAuth 2.0 client only supports client authentication method '%s', however that method is not supported by this server.", oidcClient.GetTokenEndpointAuthMethod()))
|
|
}
|
|
|
|
if oidcClient.GetTokenEndpointAuthSigningAlgorithm() != fmt.Sprintf("%s", t.Header[JWTHeaderKeyAlgorithm]) {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The 'client_assertion' uses signing algorithm '%s' but the requested OAuth 2.0 Client enforces signing algorithm '%s'.", t.Header[JWTHeaderKeyAlgorithm], oidcClient.GetTokenEndpointAuthSigningAlgorithm()))
|
|
}
|
|
|
|
switch t.Method {
|
|
case jwt.SigningMethodRS256, jwt.SigningMethodRS384, jwt.SigningMethodRS512:
|
|
return p.findClientPublicJWK(ctx, oidcClient, t, true)
|
|
case jwt.SigningMethodES256, jwt.SigningMethodES384, jwt.SigningMethodES512:
|
|
return p.findClientPublicJWK(ctx, oidcClient, t, false)
|
|
case jwt.SigningMethodPS256, jwt.SigningMethodPS384, jwt.SigningMethodPS512:
|
|
return p.findClientPublicJWK(ctx, oidcClient, t, true)
|
|
case jwt.SigningMethodHS256, jwt.SigningMethodHS384, jwt.SigningMethodHS512:
|
|
if spd, ok := oidcClient.Secret.(*schema.PasswordDigest); ok {
|
|
if secret, ok := spd.Digest.(*plaintext.Digest); ok {
|
|
return secret.Key(), nil
|
|
}
|
|
}
|
|
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("This client does not support authentication method 'client_secret_jwt' as the client secret is not in plaintext."))
|
|
default:
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The 'client_assertion' request parameter uses unsupported signing algorithm '%s'.", t.Header[JWTHeaderKeyAlgorithm]))
|
|
}
|
|
})
|
|
|
|
if err != nil {
|
|
var r *fosite.RFC6749Error
|
|
|
|
if errors.As(err, &r) {
|
|
return nil, err
|
|
}
|
|
|
|
var e *jwt.ValidationError
|
|
|
|
if errors.As(err, &e) {
|
|
rfc := fosite.ErrInvalidClient.WithHint("Unable to verify the integrity of the 'client_assertion' value.").WithWrap(err)
|
|
|
|
switch {
|
|
case e.Errors&jwt.ValidationErrorMalformed != 0:
|
|
return nil, errorsx.WithStack(rfc.WithDebug("The token is malformed."))
|
|
case e.Errors&jwt.ValidationErrorIssuedAt != 0:
|
|
return nil, errorsx.WithStack(rfc.WithDebug("The token was used before it was issued."))
|
|
case e.Errors&jwt.ValidationErrorExpired != 0:
|
|
return nil, errorsx.WithStack(rfc.WithDebug("The token is expired."))
|
|
case e.Errors&jwt.ValidationErrorNotValidYet != 0:
|
|
return nil, errorsx.WithStack(rfc.WithDebug("The token isn't valid yet."))
|
|
case e.Errors&jwt.ValidationErrorSignatureInvalid != 0:
|
|
return nil, errorsx.WithStack(rfc.WithDebug("The signature is invalid."))
|
|
}
|
|
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("Unable to verify the integrity of the 'client_assertion' value.").WithWrap(err).WithDebug(err.Error()))
|
|
}
|
|
|
|
return nil, err
|
|
} else if err = token.Claims.Valid(); err != nil {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("Unable to verify the request object because its claims could not be validated, check if the expiry time is set correctly.").WithWrap(err).WithDebug(err.Error()))
|
|
}
|
|
|
|
claims := token.Claims.(jwt.MapClaims)
|
|
|
|
tokenURL := p.Config.GetTokenURL(ctx)
|
|
|
|
var jti string
|
|
|
|
if !claims.VerifyIssuer(clientID, true) {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("Claim 'iss' from 'client_assertion' must match the 'client_id' of the OAuth 2.0 Client."))
|
|
} else if tokenURL == "" {
|
|
return nil, errorsx.WithStack(fosite.ErrMisconfiguration.WithHint("The authorization server's token endpoint URL has not been set."))
|
|
} else if sub, ok := claims[ClaimSubject].(string); !ok || sub != clientID {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("Claim 'sub' from 'client_assertion' must match the 'client_id' of the OAuth 2.0 Client."))
|
|
} else if jti, ok = claims[ClaimJWTID].(string); !ok || len(jti) == 0 {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("Claim 'jti' from 'client_assertion' must be set but is not."))
|
|
} else if p.Store.ClientAssertionJWTValid(ctx, jti) != nil {
|
|
return nil, errorsx.WithStack(fosite.ErrJTIKnown.WithHint("Claim 'jti' from 'client_assertion' MUST only be used once."))
|
|
}
|
|
|
|
err = nil
|
|
|
|
var expiry int64
|
|
|
|
switch exp := claims[ClaimExpirationTime].(type) {
|
|
case float64:
|
|
expiry = int64(exp)
|
|
case int64:
|
|
expiry = exp
|
|
case json.Number:
|
|
expiry, err = exp.Int64()
|
|
default:
|
|
err = fosite.ErrInvalidClient.WithHint("Unable to type assert the expiry time from claims. This should not happen as we validate the expiry time already earlier with token.Claims.Valid()")
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, errorsx.WithStack(err)
|
|
}
|
|
|
|
if err = p.Store.SetClientAssertionJWT(ctx, jti, time.Unix(expiry, 0)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var found bool
|
|
|
|
if auds, ok := claims[ClaimAudience].([]any); ok {
|
|
for _, aud := range auds {
|
|
if a, ok := aud.(string); ok && a == tokenURL {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("Claim 'audience' from 'client_assertion' must match the authorization server's token endpoint '%s'.", tokenURL))
|
|
}
|
|
|
|
return client, nil
|
|
} else if len(assertionType) > 0 {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Unknown client_assertion_type '%s'.", assertionType))
|
|
}
|
|
|
|
clientID, clientSecret, err := clientCredentialsFromRequest(r, form)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if client, err = p.Store.GetClient(ctx, clientID); err != nil {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithWrap(err).WithDebug(err.Error()))
|
|
}
|
|
|
|
if oidcClient, ok := client.(fosite.OpenIDConnectClient); ok {
|
|
method := oidcClient.GetTokenEndpointAuthMethod()
|
|
|
|
if form.Get(FormParameterClientID) != "" && form.Get(FormParameterClientSecret) != "" && method != ClientAuthMethodClientSecretPost {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The OAuth 2.0 Client supports client authentication method '%s', but method 'client_secret_post' was requested. You must configure the OAuth 2.0 client's 'token_endpoint_auth_method' value to accept 'client_secret_post'.", method))
|
|
} else if _, secret, basicOk := r.BasicAuth(); basicOk && secret != "" && method != ClientAuthMethodClientSecretBasic {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The OAuth 2.0 Client supports client authentication method '%s', but method 'client_secret_basic' was requested. You must configure the OAuth 2.0 client's 'token_endpoint_auth_method' value to accept 'client_secret_basic'.", method))
|
|
} else if method != ClientAuthMethodNone && client.IsPublic() {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The OAuth 2.0 Client supports client authentication method '%s', but method 'none' was requested. You must configure the OAuth 2.0 client's 'token_endpoint_auth_method' value to accept 'none'.", method))
|
|
}
|
|
}
|
|
|
|
if client.IsPublic() {
|
|
return client, nil
|
|
}
|
|
|
|
if err = p.checkClientSecret(ctx, client, []byte(clientSecret)); err != nil {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithWrap(err).WithDebug(err.Error()))
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func (p *OpenIDConnectProvider) checkClientSecret(ctx context.Context, client fosite.Client, clientSecret []byte) (err error) {
|
|
if err = p.Config.GetSecretsHasher(ctx).Compare(ctx, client.GetHashedSecret(), clientSecret); err == nil {
|
|
return nil
|
|
}
|
|
|
|
cc, ok := client.(fosite.ClientWithSecretRotation)
|
|
if !ok {
|
|
return err
|
|
}
|
|
|
|
for _, hash := range cc.GetRotatedHashes() {
|
|
if err = p.Config.GetSecretsHasher(ctx).Compare(ctx, hash, clientSecret); err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (p *OpenIDConnectProvider) findClientPublicJWK(ctx context.Context, oidcClient fosite.OpenIDConnectClient, t *jwt.Token, expectsRSAKey bool) (any, error) {
|
|
if set := oidcClient.GetJSONWebKeys(); set != nil {
|
|
return findPublicKey(t, set, expectsRSAKey)
|
|
}
|
|
|
|
if location := oidcClient.GetJSONWebKeysURI(); len(location) > 0 {
|
|
keys, err := p.Config.GetJWKSFetcherStrategy(ctx).Resolve(ctx, location, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if key, err := findPublicKey(t, keys, expectsRSAKey); err == nil {
|
|
return key, nil
|
|
}
|
|
|
|
keys, err = p.Config.GetJWKSFetcherStrategy(ctx).Resolve(ctx, location, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return findPublicKey(t, keys, expectsRSAKey)
|
|
}
|
|
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("The OAuth 2.0 Client has no JSON Web Keys set registered, but they are needed to complete the request."))
|
|
}
|
|
|
|
func findPublicKey(t *jwt.Token, set *jose.JSONWebKeySet, expectsRSAKey bool) (any, error) {
|
|
keys := set.Keys
|
|
if len(keys) == 0 {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("The retrieved JSON Web Key Set does not contain any key."))
|
|
}
|
|
|
|
kid, ok := t.Header[JWTHeaderKeyIdentifier].(string)
|
|
if ok {
|
|
keys = set.Key(kid)
|
|
}
|
|
|
|
if len(keys) == 0 {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("The JSON Web Token uses signing key with kid '%s', which could not be found.", kid))
|
|
}
|
|
|
|
for _, key := range keys {
|
|
if key.Use != KeyUseSignature {
|
|
continue
|
|
}
|
|
|
|
if expectsRSAKey {
|
|
if k, ok := key.Key.(*rsa.PublicKey); ok {
|
|
return k, nil
|
|
}
|
|
} else {
|
|
if k, ok := key.Key.(*ecdsa.PublicKey); ok {
|
|
return k, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
if expectsRSAKey {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Unable to find RSA public key with use='sig' for kid '%s' in JSON Web Key Set.", kid))
|
|
} else {
|
|
return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Unable to find ECDSA public key with use='sig' for kid '%s' in JSON Web Key Set.", kid))
|
|
}
|
|
}
|
|
|
|
func clientCredentialsFromRequest(r *http.Request, form url.Values) (clientID, clientSecret string, err error) {
|
|
if id, secret, ok := r.BasicAuth(); !ok {
|
|
return clientCredentialsFromRequestBody(form, true)
|
|
} else if clientID, err = url.QueryUnescape(id); err != nil {
|
|
return "", "", errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("The client id in the HTTP authorization header could not be decoded from 'application/x-www-form-urlencoded'.").WithWrap(err).WithDebug(err.Error()))
|
|
} else if clientSecret, err = url.QueryUnescape(secret); err != nil {
|
|
return "", "", errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("The client secret in the HTTP authorization header could not be decoded from 'application/x-www-form-urlencoded'.").WithWrap(err).WithDebug(err.Error()))
|
|
}
|
|
|
|
return clientID, clientSecret, nil
|
|
}
|
|
|
|
func clientCredentialsFromRequestBody(form url.Values, forceID bool) (clientID, clientSecret string, err error) {
|
|
clientID = form.Get(FormParameterClientID)
|
|
clientSecret = form.Get(FormParameterClientSecret)
|
|
|
|
if clientID == "" && forceID {
|
|
return "", "", errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Client credentials missing or malformed in both HTTP Authorization header and HTTP POST body."))
|
|
}
|
|
|
|
return clientID, clientSecret, nil
|
|
}
|