api/vendor/github.com/ory/fosite/authorize_request_handler.go
2020-06-26 11:11:27 +02:00

277 lines
11 KiB
Go

/*
* Copyright © 2015-2018 Aeneas Rekkas <aeneas+oss@aeneas.io>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author Aeneas Rekkas <aeneas+oss@aeneas.io>
* @copyright 2015-2018 Aeneas Rekkas <aeneas+oss@aeneas.io>
* @license Apache-2.0
*
*/
package fosite
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"strings"
jwt "github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
"github.com/ory/go-convenience/stringslice"
"github.com/ory/go-convenience/stringsx"
)
func (f *Fosite) authorizeRequestParametersFromOpenIDConnectRequest(request *AuthorizeRequest) error {
var scope Arguments = stringsx.Splitx(request.Form.Get("scope"), " ")
// Even if a scope parameter is present in the Request Object value, a scope parameter MUST always be passed using
// the OAuth 2.0 request syntax containing the openid scope value to indicate to the underlying OAuth 2.0 logic that this is an OpenID Connect request.
// Source: http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth
if !scope.Has("openid") {
return nil
}
if len(request.Form.Get("request")+request.Form.Get("request_uri")) == 0 {
return nil
} else if len(request.Form.Get("request")) > 0 && len(request.Form.Get("request_uri")) > 0 {
return errors.WithStack(ErrInvalidRequest.WithHint(`OpenID Connect parameters "request" and "request_uri" were both given, but you can use at most one.`))
}
oidcClient, ok := request.Client.(OpenIDConnectClient)
if !ok {
if len(request.Form.Get("request_uri")) > 0 {
return errors.WithStack(ErrRequestURINotSupported.WithHint(`OpenID Connect "request_uri" context was given, but the OAuth 2.0 Client does not implement advanced OpenID Connect capabilities.`))
}
return errors.WithStack(ErrRequestNotSupported.WithHint(`OpenID Connect "request" context was given, but the OAuth 2.0 Client does not implement advanced OpenID Connect capabilities.`))
}
if oidcClient.GetJSONWebKeys() == nil && len(oidcClient.GetJSONWebKeysURI()) == 0 {
return errors.WithStack(ErrInvalidRequest.WithHint(`OpenID Connect "request" or "request_uri" context was given, but the OAuth 2.0 Client does not have any JSON Web Keys registered.`))
}
assertion := request.Form.Get("request")
if location := request.Form.Get("request_uri"); len(location) > 0 {
if !stringslice.Has(oidcClient.GetRequestURIs(), location) {
return errors.WithStack(ErrInvalidRequestURI.WithHint(fmt.Sprintf("Request URI \"%s\" is not whitelisted by the OAuth 2.0 Client.", location)))
}
hc := f.HTTPClient
if hc == nil {
hc = http.DefaultClient
}
response, err := hc.Get(location)
if err != nil {
return errors.WithStack(ErrInvalidRequestURI.WithHintf(`Unable to fetch OpenID Connect request parameters from "request_uri" because %s.`, err.Error()))
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return errors.WithStack(ErrInvalidRequestURI.WithHintf(`Unable to fetch OpenID Connect request parameters from "request_uri" because status code "%d" was expected, but got "%d".`, http.StatusOK, response.StatusCode))
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return errors.WithStack(ErrInvalidRequestURI.WithHintf(`Unable to fetch OpenID Connect request parameters from "request_uri" because error %s occurred during body parsing.`, err))
}
assertion = string(body)
}
token, err := jwt.ParseWithClaims(assertion, new(jwt.MapClaims), func(t *jwt.Token) (interface{}, error) {
if oidcClient.GetRequestObjectSigningAlgorithm() != fmt.Sprintf("%s", t.Header["alg"]) {
return nil, errors.WithStack(ErrInvalidRequestObject.WithHintf(`The request object uses signing algorithm %s, but the requested OAuth 2.0 Client enforces signing algorithm %s.`, t.Header["alg"], oidcClient.GetRequestObjectSigningAlgorithm()))
}
if t.Method == jwt.SigningMethodNone {
return jwt.UnsafeAllowNoneSignatureType, nil
}
switch t.Method.(type) {
case *jwt.SigningMethodRSA:
key, err := f.findClientPublicJWK(oidcClient, t, true)
if err != nil {
return nil, errors.WithStack(ErrInvalidRequestObject.WithHintf("Unable to retrieve RSA signing key from OAuth 2.0 Client because %s.", err))
}
return key, nil
case *jwt.SigningMethodECDSA:
key, err := f.findClientPublicJWK(oidcClient, t, false)
if err != nil {
return nil, errors.WithStack(ErrInvalidRequestObject.WithHintf("Unable to retrieve ECDSA signing key from OAuth 2.0 Client because %s.", err))
}
return key, nil
case *jwt.SigningMethodRSAPSS:
key, err := f.findClientPublicJWK(oidcClient, t, true)
if err != nil {
return nil, errors.WithStack(ErrInvalidRequestObject.WithHintf("Unable to retrieve RSA signing key from OAuth 2.0 Client because %s.", err))
}
return key, nil
default:
return nil, errors.WithStack(ErrInvalidRequestObject.WithHintf(`This request object uses unsupported signing algorithm "%s"."`, t.Header["alg"]))
}
})
if err != nil {
// Do not re-process already enhanced errors
if e, ok := errors.Cause(err).(*jwt.ValidationError); ok {
if e.Inner != nil {
return e.Inner
}
return errors.WithStack(ErrInvalidRequestObject.WithHintf("Unable to verify the request object's signature.").WithDebug(err.Error()))
}
return err
} else if err := token.Claims.Valid(); err != nil {
return errors.WithStack(ErrInvalidRequestObject.WithHint("Unable to verify the request object because its claims could not be validated, check if the expiry time is set correctly.").WithDebug(err.Error()))
}
claims, ok := token.Claims.(*jwt.MapClaims)
if !ok {
return errors.WithStack(ErrInvalidRequestObject.WithHint("Unable to type assert claims from request object.").WithDebugf(`Got claims of type %T but expected type "*jwt.MapClaims".`, token.Claims))
}
for k, v := range *claims {
request.Form.Set(k, fmt.Sprintf("%s", v))
}
claimScope := stringsx.Splitx(request.Form.Get("scope"), " ")
for _, s := range scope {
if !stringslice.Has(claimScope, s) {
claimScope = append(claimScope, s)
}
}
request.Form.Set("scope", strings.Join(claimScope, " "))
return nil
}
func (f *Fosite) validateAuthorizeRedirectURI(r *http.Request, request *AuthorizeRequest) error {
// Fetch redirect URI from request
rawRedirURI, err := GetRedirectURIFromRequestValues(request.Form)
if err != nil {
return err
}
// Validate redirect uri
redirectURI, err := MatchRedirectURIWithClientRedirectURIs(rawRedirURI, request.Client)
if err != nil {
return err
} else if !IsValidRedirectURI(redirectURI) {
return errors.WithStack(ErrInvalidRequest.WithHintf(`The redirect URI "%s" contains an illegal character (for example #) or is otherwise invalid.`, redirectURI))
}
request.RedirectURI = redirectURI
return nil
}
func (f *Fosite) validateAuthorizeScope(r *http.Request, request *AuthorizeRequest) error {
scope := stringsx.Splitx(request.Form.Get("scope"), " ")
for _, permission := range scope {
if !f.ScopeStrategy(request.Client.GetScopes(), permission) {
return errors.WithStack(ErrInvalidScope.WithHintf(`The OAuth 2.0 Client is not allowed to request scope "%s".`, permission))
}
}
request.SetRequestedScopes(scope)
return nil
}
func (f *Fosite) validateResponseTypes(r *http.Request, request *AuthorizeRequest) error {
// https://tools.ietf.org/html/rfc6749#section-3.1.1
// Extension response types MAY contain a space-delimited (%x20) list of
// values, where the order of values does not matter (e.g., response
// type "a b" is the same as "b a"). The meaning of such composite
// response types is defined by their respective specifications.
responseTypes := RemoveEmpty(stringsx.Splitx(r.Form.Get("response_type"), " "))
if len(responseTypes) == 0 {
return errors.WithStack(ErrUnsupportedResponseType.WithHint(`The request is missing the "response_type"" parameter.`))
}
var found bool
for _, t := range request.GetClient().GetResponseTypes() {
if Arguments(responseTypes).Matches(RemoveEmpty(stringsx.Splitx(t, " "))...) {
found = true
break
}
}
if !found {
return errors.WithStack(ErrUnsupportedResponseType.WithHintf("The client is not allowed to request response_type \"%s\".", r.Form.Get("response_type")))
}
request.ResponseTypes = responseTypes
return nil
}
func (f *Fosite) NewAuthorizeRequest(ctx context.Context, r *http.Request) (AuthorizeRequester, error) {
request := &AuthorizeRequest{
ResponseTypes: Arguments{},
HandledResponseTypes: Arguments{},
Request: *NewRequest(),
}
if err := r.ParseMultipartForm(1 << 20); err != nil && err != http.ErrNotMultipart {
return request, errors.WithStack(ErrInvalidRequest.WithHint("Unable to parse HTTP body, make sure to send a properly formatted form request body.").WithDebug(err.Error()))
}
request.Form = r.Form
// Save state to the request to be returned in error conditions (https://github.com/ory/hydra/issues/1642)
state := request.Form.Get("state")
request.State = state
client, err := f.Store.GetClient(ctx, request.GetRequestForm().Get("client_id"))
if err != nil {
return request, errors.WithStack(ErrInvalidClient.WithHint("The requested OAuth 2.0 Client does not exist.").WithDebug(err.Error()))
}
request.Client = client
if err := f.authorizeRequestParametersFromOpenIDConnectRequest(request); err != nil {
return request, err
}
if err := f.validateAuthorizeRedirectURI(r, request); err != nil {
return request, err
}
if err := f.validateAuthorizeScope(r, request); err != nil {
return request, err
}
if err := f.validateAuthorizeAudience(r, request); err != nil {
return request, err
}
if len(request.Form.Get("registration")) > 0 {
return request, errors.WithStack(ErrRegistrationNotSupported)
}
if err := f.validateResponseTypes(r, request); err != nil {
return request, err
}
// rfc6819 4.4.1.8. Threat: CSRF Attack against redirect-uri
// The "state" parameter should be used to link the authorization
// request with the redirect URI used to deliver the access token (Section 5.3.5).
//
// https://tools.ietf.org/html/rfc6819#section-4.4.1.8
// The "state" parameter should not be guessable
if len(state) < MinParameterEntropy {
// We're assuming that using less then 8 characters for the state can not be considered "unguessable"
return request, errors.WithStack(ErrInvalidState.WithHintf(`Request parameter "state" must be at least be %d characters long to ensure sufficient entropy.`, MinParameterEntropy))
}
return request, nil
}