authelia/internal/oidc/authentication.go

379 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
}
// Serves as validation that the structures in this file implement the fosite interfaces.
var (
_ fosite.Hasher = (*Hasher)(nil)
)