...

Source file src/github.com/ory/fosite/client_authentication.go

Documentation: github.com/ory/fosite

     1  /*
     2   * Copyright © 2017-2018 Aeneas Rekkas <aeneas+oss@aeneas.io>
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   *
    16   * @author		Aeneas Rekkas <aeneas+oss@aeneas.io>
    17   * @Copyright 	2017-2018 Aeneas Rekkas <aeneas+oss@aeneas.io>
    18   * @license 	Apache-2.0
    19   *
    20   */
    21  
    22  package fosite
    23  
    24  import (
    25  	"context"
    26  	"crypto/ecdsa"
    27  	"crypto/rsa"
    28  	"encoding/json"
    29  	"fmt"
    30  	"net/http"
    31  	"net/url"
    32  	"time"
    33  
    34  	"github.com/ory/x/errorsx"
    35  
    36  	"github.com/ory/fosite/token/jwt"
    37  	"github.com/pkg/errors"
    38  	jose "gopkg.in/square/go-jose.v2"
    39  )
    40  
    41  // ClientAuthenticationStrategy provides a method signature for authenticating a client request
    42  type ClientAuthenticationStrategy func(context.Context, *http.Request, url.Values) (Client, error)
    43  
    44  const clientAssertionJWTBearerType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
    45  
    46  func (f *Fosite) findClientPublicJWK(oidcClient OpenIDConnectClient, t *jwt.Token, expectsRSAKey bool) (interface{}, error) {
    47  	if set := oidcClient.GetJSONWebKeys(); set != nil {
    48  		return findPublicKey(t, set, expectsRSAKey)
    49  	}
    50  
    51  	if location := oidcClient.GetJSONWebKeysURI(); len(location) > 0 {
    52  		keys, err := f.JWKSFetcherStrategy.Resolve(location, false)
    53  		if err != nil {
    54  			return nil, err
    55  		}
    56  
    57  		if key, err := findPublicKey(t, keys, expectsRSAKey); err == nil {
    58  			return key, nil
    59  		}
    60  
    61  		keys, err = f.JWKSFetcherStrategy.Resolve(location, true)
    62  		if err != nil {
    63  			return nil, err
    64  		}
    65  
    66  		return findPublicKey(t, keys, expectsRSAKey)
    67  	}
    68  
    69  	return nil, errorsx.WithStack(ErrInvalidClient.WithHint("The OAuth 2.0 Client has no JSON Web Keys set registered, but they are needed to complete the request."))
    70  }
    71  
    72  // AuthenticateClient authenticates client requests using the configured strategy
    73  // `Fosite.ClientAuthenticationStrategy`, if nil it uses `Fosite.DefaultClientAuthenticationStrategy`
    74  func (f *Fosite) AuthenticateClient(ctx context.Context, r *http.Request, form url.Values) (Client, error) {
    75  	if f.ClientAuthenticationStrategy == nil {
    76  		return f.DefaultClientAuthenticationStrategy(ctx, r, form)
    77  	}
    78  	return f.ClientAuthenticationStrategy(ctx, r, form)
    79  }
    80  
    81  // DefaultClientAuthenticationStrategy provides the fosite's default client authentication strategy,
    82  // HTTP Basic Authentication and JWT Bearer
    83  func (f *Fosite) DefaultClientAuthenticationStrategy(ctx context.Context, r *http.Request, form url.Values) (Client, error) {
    84  	if assertionType := form.Get("client_assertion_type"); assertionType == clientAssertionJWTBearerType {
    85  		assertion := form.Get("client_assertion")
    86  		if len(assertion) == 0 {
    87  			return nil, errorsx.WithStack(ErrInvalidRequest.WithHintf("The client_assertion request parameter must be set when using client_assertion_type of '%s'.", clientAssertionJWTBearerType))
    88  		}
    89  
    90  		var clientID string
    91  		var client Client
    92  
    93  		token, err := jwt.ParseWithClaims(assertion, jwt.MapClaims{}, func(t *jwt.Token) (interface{}, error) {
    94  			var err error
    95  			clientID, _, err = clientCredentialsFromRequestBody(form, false)
    96  			if err != nil {
    97  				return nil, err
    98  			}
    99  
   100  			if clientID == "" {
   101  				claims := t.Claims
   102  				if sub, ok := claims["sub"].(string); !ok {
   103  					return nil, errorsx.WithStack(ErrInvalidClient.WithHint("The claim 'sub' from the client_assertion JSON Web Token is undefined."))
   104  				} else {
   105  					clientID = sub
   106  				}
   107  			}
   108  
   109  			client, err = f.Store.GetClient(ctx, clientID)
   110  			if err != nil {
   111  				return nil, errorsx.WithStack(ErrInvalidClient.WithWrap(err).WithDebug(err.Error()))
   112  			}
   113  
   114  			oidcClient, ok := client.(OpenIDConnectClient)
   115  			if !ok {
   116  				return nil, errorsx.WithStack(ErrInvalidRequest.WithHint("The server configuration does not support OpenID Connect specific authentication methods."))
   117  			}
   118  
   119  			switch oidcClient.GetTokenEndpointAuthMethod() {
   120  			case "private_key_jwt":
   121  				break
   122  			case "none":
   123  				return nil, errorsx.WithStack(ErrInvalidClient.WithHint("This requested OAuth 2.0 client does not support client authentication, however 'client_assertion' was provided in the request."))
   124  			case "client_secret_post":
   125  				fallthrough
   126  			case "client_secret_basic":
   127  				return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("This requested OAuth 2.0 client only supports client authentication method '%s', however 'client_assertion' was provided in the request.", oidcClient.GetTokenEndpointAuthMethod()))
   128  			case "client_secret_jwt":
   129  				fallthrough
   130  			default:
   131  				return nil, errorsx.WithStack(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()))
   132  			}
   133  
   134  			if oidcClient.GetTokenEndpointAuthSigningAlgorithm() != fmt.Sprintf("%s", t.Header["alg"]) {
   135  				return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("The 'client_assertion' uses signing algorithm '%s' but the requested OAuth 2.0 Client enforces signing algorithm '%s'.", t.Header["alg"], oidcClient.GetTokenEndpointAuthSigningAlgorithm()))
   136  			}
   137  			switch t.Method {
   138  			case jose.RS256, jose.RS384, jose.RS512:
   139  				return f.findClientPublicJWK(oidcClient, t, true)
   140  			case jose.ES256, jose.ES384, jose.ES512:
   141  				return f.findClientPublicJWK(oidcClient, t, false)
   142  			case jose.PS256, jose.PS384, jose.PS512:
   143  				return f.findClientPublicJWK(oidcClient, t, true)
   144  			case jose.HS256, jose.HS384, jose.HS512:
   145  				return nil, errorsx.WithStack(ErrInvalidClient.WithHint("This authorization server does not support client authentication method 'client_secret_jwt'."))
   146  			default:
   147  				return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("The 'client_assertion' request parameter uses unsupported signing algorithm '%s'.", t.Header["alg"]))
   148  			}
   149  		})
   150  		if err != nil {
   151  			// Do not re-process already enhanced errors
   152  			var e *jwt.ValidationError
   153  			if errors.As(err, &e) {
   154  				if e.Inner != nil {
   155  					return nil, e.Inner
   156  				}
   157  				return nil, errorsx.WithStack(ErrInvalidClient.WithHint("Unable to verify the integrity of the 'client_assertion' value.").WithWrap(err).WithDebug(err.Error()))
   158  			}
   159  			return nil, err
   160  		} else if err := token.Claims.Valid(); err != nil {
   161  			return nil, errorsx.WithStack(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()))
   162  		}
   163  
   164  		claims := token.Claims
   165  		var jti string
   166  		if !claims.VerifyIssuer(clientID, true) {
   167  			return nil, errorsx.WithStack(ErrInvalidClient.WithHint("Claim 'iss' from 'client_assertion' must match the 'client_id' of the OAuth 2.0 Client."))
   168  		} else if f.TokenURL == "" {
   169  			return nil, errorsx.WithStack(ErrMisconfiguration.WithHint("The authorization server's token endpoint URL has not been set."))
   170  		} else if sub, ok := claims["sub"].(string); !ok || sub != clientID {
   171  			return nil, errorsx.WithStack(ErrInvalidClient.WithHint("Claim 'sub' from 'client_assertion' must match the 'client_id' of the OAuth 2.0 Client."))
   172  		} else if jti, ok = claims["jti"].(string); !ok || len(jti) == 0 {
   173  			return nil, errorsx.WithStack(ErrInvalidClient.WithHint("Claim 'jti' from 'client_assertion' must be set but is not."))
   174  		} else if f.Store.ClientAssertionJWTValid(ctx, jti) != nil {
   175  			return nil, errorsx.WithStack(ErrJTIKnown.WithHint("Claim 'jti' from 'client_assertion' MUST only be used once."))
   176  		}
   177  
   178  		// type conversion according to jwt.MapClaims.VerifyExpiresAt
   179  		var expiry int64
   180  		err = nil
   181  		switch exp := claims["exp"].(type) {
   182  		case float64:
   183  			expiry = int64(exp)
   184  		case int64:
   185  			expiry = exp
   186  		case json.Number:
   187  			expiry, err = exp.Int64()
   188  		default:
   189  			err = 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()")
   190  		}
   191  
   192  		if err != nil {
   193  			return nil, errorsx.WithStack(err)
   194  		}
   195  		if err := f.Store.SetClientAssertionJWT(ctx, jti, time.Unix(expiry, 0)); err != nil {
   196  			return nil, err
   197  		}
   198  
   199  		if auds, ok := claims["aud"].([]interface{}); !ok {
   200  			if !claims.VerifyAudience(f.TokenURL, true) {
   201  				return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("Claim 'audience' from 'client_assertion' must match the authorization server's token endpoint '%s'.", f.TokenURL))
   202  			}
   203  		} else {
   204  			var found bool
   205  			for _, aud := range auds {
   206  				if a, ok := aud.(string); ok && a == f.TokenURL {
   207  					found = true
   208  					break
   209  				}
   210  			}
   211  
   212  			if !found {
   213  				return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("Claim 'audience' from 'client_assertion' must match the authorization server's token endpoint '%s'.", f.TokenURL))
   214  			}
   215  		}
   216  
   217  		return client, nil
   218  	} else if len(assertionType) > 0 {
   219  		return nil, errorsx.WithStack(ErrInvalidRequest.WithHintf("Unknown client_assertion_type '%s'.", assertionType))
   220  	}
   221  
   222  	clientID, clientSecret, err := clientCredentialsFromRequest(r, form)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  
   227  	client, err := f.Store.GetClient(ctx, clientID)
   228  	if err != nil {
   229  		return nil, errorsx.WithStack(ErrInvalidClient.WithWrap(err).WithDebug(err.Error()))
   230  	}
   231  
   232  	if oidcClient, ok := client.(OpenIDConnectClient); !ok {
   233  		// If this isn't an OpenID Connect client then we actually don't care about any of this, just continue!
   234  	} else if ok && form.Get("client_id") != "" && form.Get("client_secret") != "" && oidcClient.GetTokenEndpointAuthMethod() != "client_secret_post" {
   235  		return nil, errorsx.WithStack(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'.", oidcClient.GetTokenEndpointAuthMethod()))
   236  	} else if _, secret, basicOk := r.BasicAuth(); basicOk && ok && secret != "" && oidcClient.GetTokenEndpointAuthMethod() != "client_secret_basic" {
   237  		return nil, errorsx.WithStack(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'.", oidcClient.GetTokenEndpointAuthMethod()))
   238  	} else if ok && oidcClient.GetTokenEndpointAuthMethod() != "none" && client.IsPublic() {
   239  		return nil, errorsx.WithStack(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'.", oidcClient.GetTokenEndpointAuthMethod()))
   240  	}
   241  
   242  	if client.IsPublic() {
   243  		return client, nil
   244  	}
   245  
   246  	// Enforce client authentication
   247  	if err := f.checkClientSecret(ctx, client, []byte(clientSecret)); err != nil {
   248  		return nil, errorsx.WithStack(ErrInvalidClient.WithWrap(err).WithDebug(err.Error()))
   249  	}
   250  
   251  	return client, nil
   252  }
   253  
   254  func (f *Fosite) checkClientSecret(ctx context.Context, client Client, clientSecret []byte) error {
   255  	var err error
   256  	err = f.Hasher.Compare(ctx, client.GetHashedSecret(), clientSecret)
   257  	if err == nil {
   258  		return nil
   259  	}
   260  	cc, ok := client.(ClientWithSecretRotation)
   261  	if !ok {
   262  		return err
   263  	}
   264  	for _, hash := range cc.GetRotatedHashes() {
   265  		err = f.Hasher.Compare(ctx, hash, clientSecret)
   266  		if err == nil {
   267  			return nil
   268  		}
   269  	}
   270  
   271  	return err
   272  }
   273  
   274  func findPublicKey(t *jwt.Token, set *jose.JSONWebKeySet, expectsRSAKey bool) (interface{}, error) {
   275  	keys := set.Keys
   276  	if len(keys) == 0 {
   277  		return nil, errorsx.WithStack(ErrInvalidRequest.WithHintf("The retrieved JSON Web Key Set does not contain any keys."))
   278  	}
   279  
   280  	kid, ok := t.Header["kid"].(string)
   281  	if ok {
   282  		keys = set.Key(kid)
   283  	}
   284  
   285  	if len(keys) == 0 {
   286  		return nil, errorsx.WithStack(ErrInvalidRequest.WithHintf("The JSON Web Token uses signing key with kid '%s', which could not be found.", kid))
   287  	}
   288  
   289  	for _, key := range keys {
   290  		if key.Use != "sig" {
   291  			continue
   292  		}
   293  		if expectsRSAKey {
   294  			if k, ok := key.Key.(*rsa.PublicKey); ok {
   295  				return k, nil
   296  			}
   297  		} else {
   298  			if k, ok := key.Key.(*ecdsa.PublicKey); ok {
   299  				return k, nil
   300  			}
   301  		}
   302  	}
   303  
   304  	if expectsRSAKey {
   305  		return nil, errorsx.WithStack(ErrInvalidRequest.WithHintf("Unable to find RSA public key with use='sig' for kid '%s' in JSON Web Key Set.", kid))
   306  	} else {
   307  		return nil, errorsx.WithStack(ErrInvalidRequest.WithHintf("Unable to find ECDSA public key with use='sig' for kid '%s' in JSON Web Key Set.", kid))
   308  	}
   309  }
   310  
   311  func clientCredentialsFromRequest(r *http.Request, form url.Values) (clientID, clientSecret string, err error) {
   312  	if id, secret, ok := r.BasicAuth(); !ok {
   313  		return clientCredentialsFromRequestBody(form, true)
   314  	} else if clientID, err = url.QueryUnescape(id); err != nil {
   315  		return "", "", errorsx.WithStack(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()))
   316  	} else if clientSecret, err = url.QueryUnescape(secret); err != nil {
   317  		return "", "", errorsx.WithStack(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()))
   318  	}
   319  
   320  	return clientID, clientSecret, nil
   321  }
   322  
   323  func clientCredentialsFromRequestBody(form url.Values, forceID bool) (clientID, clientSecret string, err error) {
   324  	clientID = form.Get("client_id")
   325  	clientSecret = form.Get("client_secret")
   326  
   327  	if clientID == "" && forceID {
   328  		return "", "", errorsx.WithStack(ErrInvalidRequest.WithHint("Client credentials missing or malformed in both HTTP Authorization header and HTTP POST body."))
   329  	}
   330  
   331  	return clientID, clientSecret, nil
   332  }
   333  

View as plain text