...

Source file src/github.com/ory/fosite/handler/openid/validator.go

Documentation: github.com/ory/fosite/handler/openid

     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 openid
    23  
    24  import (
    25  	"context"
    26  	"net/url"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/ory/x/errorsx"
    32  
    33  	"github.com/pkg/errors"
    34  
    35  	"github.com/ory/fosite"
    36  	"github.com/ory/fosite/token/jwt"
    37  	"github.com/ory/go-convenience/stringslice"
    38  )
    39  
    40  type OpenIDConnectRequestValidator struct {
    41  	AllowedPrompt       []string
    42  	Strategy            jwt.JWTStrategy
    43  	IsRedirectURISecure func(*url.URL) bool
    44  }
    45  
    46  func NewOpenIDConnectRequestValidator(prompt []string, strategy jwt.JWTStrategy) *OpenIDConnectRequestValidator {
    47  	if len(prompt) == 0 {
    48  		prompt = []string{"login", "none", "consent", "select_account"}
    49  	}
    50  
    51  	return &OpenIDConnectRequestValidator{
    52  		AllowedPrompt: prompt,
    53  		Strategy:      strategy,
    54  	}
    55  }
    56  
    57  func (v *OpenIDConnectRequestValidator) WithRedirectSecureChecker(checker func(*url.URL) bool) *OpenIDConnectRequestValidator {
    58  	v.IsRedirectURISecure = checker
    59  	return v
    60  }
    61  
    62  func (v *OpenIDConnectRequestValidator) secureChecker() func(*url.URL) bool {
    63  	if v.IsRedirectURISecure == nil {
    64  		v.IsRedirectURISecure = fosite.IsRedirectURISecure
    65  	}
    66  	return v.IsRedirectURISecure
    67  }
    68  
    69  func (v *OpenIDConnectRequestValidator) ValidatePrompt(ctx context.Context, req fosite.AuthorizeRequester) error {
    70  	// prompt is case sensitive!
    71  	prompt := fosite.RemoveEmpty(strings.Split(req.GetRequestForm().Get("prompt"), " "))
    72  
    73  	if req.GetClient().IsPublic() {
    74  		// Threat: Malicious Client Obtains Existing Authorization by Fraud
    75  		// https://tools.ietf.org/html/rfc6819#section-4.2.3
    76  		//
    77  		//  Authorization servers should not automatically process repeat
    78  		//  authorizations to public clients unless the client is validated
    79  		//  using a pre-registered redirect URI
    80  
    81  		// Client Impersonation
    82  		// https://tools.ietf.org/html/rfc8252#section-8.6#
    83  		//
    84  		//  As stated in Section 10.2 of OAuth 2.0 [RFC6749], the authorization
    85  		//  server SHOULD NOT process authorization requests automatically
    86  		//  without user consent or interaction, except when the identity of the
    87  		//  client can be assured.  This includes the case where the user has
    88  		//  previously approved an authorization request for a given client id --
    89  		//  unless the identity of the client can be proven, the request SHOULD
    90  		//  be processed as if no previous request had been approved.
    91  
    92  		if stringslice.Has(prompt, "none") {
    93  			if !v.secureChecker()(req.GetRedirectURI()) {
    94  				return errorsx.WithStack(fosite.ErrConsentRequired.WithHint("OAuth 2.0 Client is marked public and redirect uri is not considered secure (https missing), but \"prompt=none\" was requested."))
    95  			}
    96  		}
    97  	}
    98  
    99  	if !isWhitelisted(prompt, v.AllowedPrompt) {
   100  		return errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Used unknown value '%s' for prompt parameter", prompt))
   101  	}
   102  
   103  	if stringslice.Has(prompt, "none") && len(prompt) > 1 {
   104  		// If this parameter contains none with any other value, an error is returned.
   105  		return errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Parameter 'prompt' was set to 'none', but contains other values as well which is not allowed."))
   106  	}
   107  
   108  	maxAge, err := strconv.ParseInt(req.GetRequestForm().Get("max_age"), 10, 64)
   109  	if err != nil {
   110  		maxAge = 0
   111  	}
   112  
   113  	session, ok := req.GetSession().(Session)
   114  	if !ok {
   115  		return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to validate OpenID Connect request because session is not of type fosite/handler/openid.Session."))
   116  	}
   117  
   118  	claims := session.IDTokenClaims()
   119  	if claims.Subject == "" {
   120  		return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to validate OpenID Connect request because session subject is empty."))
   121  	}
   122  
   123  	// Adds a bit of wiggle room for timing issues
   124  	if claims.AuthTime.After(time.Now().UTC().Add(time.Second * 5)) {
   125  		return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to validate OpenID Connect request because authentication time is in the future."))
   126  	}
   127  
   128  	if maxAge > 0 {
   129  		if claims.AuthTime.IsZero() {
   130  			return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to validate OpenID Connect request because authentication time claim is required when max_age is set."))
   131  		} else if claims.RequestedAt.IsZero() {
   132  			return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to validate OpenID Connect request because requested at claim is required when max_age is set."))
   133  		} else if claims.AuthTime.Add(time.Second * time.Duration(maxAge)).Before(claims.RequestedAt) {
   134  			return errorsx.WithStack(fosite.ErrLoginRequired.WithDebug("Failed to validate OpenID Connect request because authentication time does not satisfy max_age time."))
   135  		}
   136  	}
   137  
   138  	if stringslice.Has(prompt, "none") {
   139  		if claims.AuthTime.IsZero() {
   140  			return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to validate OpenID Connect request because because auth_time is missing from session."))
   141  		}
   142  		if !claims.AuthTime.Equal(claims.RequestedAt) && claims.AuthTime.After(claims.RequestedAt) {
   143  			// !claims.AuthTime.Truncate(time.Second).Equal(claims.RequestedAt) && claims.AuthTime.Truncate(time.Second).Before(claims.RequestedAt) {
   144  			return errorsx.WithStack(fosite.ErrLoginRequired.WithHintf("Failed to validate OpenID Connect request because prompt was set to 'none' but auth_time ('%s') happened after the authorization request ('%s') was registered, indicating that the user was logged in during this request which is not allowed.", claims.AuthTime, claims.RequestedAt))
   145  		}
   146  	}
   147  
   148  	if stringslice.Has(prompt, "login") {
   149  		if claims.AuthTime.Before(claims.RequestedAt) {
   150  			return errorsx.WithStack(fosite.ErrLoginRequired.WithHintf("Failed to validate OpenID Connect request because prompt was set to 'login' but auth_time ('%s') happened before the authorization request ('%s') was registered, indicating that the user was not re-authenticated which is forbidden.", claims.AuthTime, claims.RequestedAt))
   151  		}
   152  	}
   153  
   154  	idTokenHint := req.GetRequestForm().Get("id_token_hint")
   155  	if idTokenHint == "" {
   156  		return nil
   157  	}
   158  
   159  	tokenHint, err := v.Strategy.Decode(ctx, idTokenHint)
   160  	var ve *jwt.ValidationError
   161  	if errors.As(err, &ve) && ve.Has(jwt.ValidationErrorExpired) {
   162  		// Expired tokens are ok
   163  	} else if err != nil {
   164  		return errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Failed to validate OpenID Connect request as decoding id token from id_token_hint parameter failed.").WithWrap(err).WithDebug(err.Error()))
   165  	}
   166  
   167  	if hintSub, _ := tokenHint.Claims["sub"].(string); hintSub == "" {
   168  		return errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Failed to validate OpenID Connect request because provided id token from id_token_hint does not have a subject."))
   169  	} else if hintSub != claims.Subject {
   170  		return errorsx.WithStack(fosite.ErrLoginRequired.WithHint("Failed to validate OpenID Connect request because the subject from provided id token from id_token_hint does not match the current session's subject."))
   171  	}
   172  
   173  	return nil
   174  }
   175  
   176  func isWhitelisted(items []string, whiteList []string) bool {
   177  	for _, item := range items {
   178  		if !stringslice.Has(whiteList, item) {
   179  			return false
   180  		}
   181  	}
   182  	return true
   183  }
   184  

View as plain text