...

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

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

     1  /*
     2   * Copyright © 2015-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 	2015-2018 Aeneas Rekkas <aeneas+oss@aeneas.io>
    18   * @license 	Apache-2.0
    19   *
    20   */
    21  
    22  package openid
    23  
    24  import (
    25  	"context"
    26  	"strconv"
    27  	"time"
    28  
    29  	"github.com/ory/x/errorsx"
    30  
    31  	"github.com/mohae/deepcopy"
    32  	"github.com/pkg/errors"
    33  
    34  	"github.com/ory/fosite"
    35  	"github.com/ory/fosite/token/jwt"
    36  	"github.com/ory/go-convenience/stringslice"
    37  )
    38  
    39  const defaultExpiryTime = time.Hour
    40  
    41  type Session interface {
    42  	// IDTokenClaims returns a pointer to claims which will be modified in-place by handlers.
    43  	// Session should store this pointer and return always the same pointer.
    44  	IDTokenClaims() *jwt.IDTokenClaims
    45  	// IDTokenHeaders returns a pointer to header values which will be modified in-place by handlers.
    46  	// Session should store this pointer and return always the same pointer.
    47  	IDTokenHeaders() *jwt.Headers
    48  
    49  	fosite.Session
    50  }
    51  
    52  // IDTokenSession is a session container for the id token
    53  type DefaultSession struct {
    54  	Claims    *jwt.IDTokenClaims
    55  	Headers   *jwt.Headers
    56  	ExpiresAt map[fosite.TokenType]time.Time
    57  	Username  string
    58  	Subject   string
    59  }
    60  
    61  func NewDefaultSession() *DefaultSession {
    62  	return &DefaultSession{
    63  		Claims: &jwt.IDTokenClaims{
    64  			RequestedAt: time.Now().UTC(),
    65  		},
    66  		Headers: &jwt.Headers{},
    67  	}
    68  }
    69  
    70  func (s *DefaultSession) Clone() fosite.Session {
    71  	if s == nil {
    72  		return nil
    73  	}
    74  
    75  	return deepcopy.Copy(s).(fosite.Session)
    76  }
    77  
    78  func (s *DefaultSession) SetExpiresAt(key fosite.TokenType, exp time.Time) {
    79  	if s.ExpiresAt == nil {
    80  		s.ExpiresAt = make(map[fosite.TokenType]time.Time)
    81  	}
    82  	s.ExpiresAt[key] = exp
    83  }
    84  
    85  func (s *DefaultSession) GetExpiresAt(key fosite.TokenType) time.Time {
    86  	if s.ExpiresAt == nil {
    87  		s.ExpiresAt = make(map[fosite.TokenType]time.Time)
    88  	}
    89  
    90  	if _, ok := s.ExpiresAt[key]; !ok {
    91  		return time.Time{}
    92  	}
    93  	return s.ExpiresAt[key]
    94  }
    95  
    96  func (s *DefaultSession) GetUsername() string {
    97  	if s == nil {
    98  		return ""
    99  	}
   100  	return s.Username
   101  }
   102  
   103  func (s *DefaultSession) SetSubject(subject string) {
   104  	s.Subject = subject
   105  }
   106  
   107  func (s *DefaultSession) GetSubject() string {
   108  	if s == nil {
   109  		return ""
   110  	}
   111  
   112  	return s.Subject
   113  }
   114  
   115  func (s *DefaultSession) IDTokenHeaders() *jwt.Headers {
   116  	if s.Headers == nil {
   117  		s.Headers = &jwt.Headers{}
   118  	}
   119  	return s.Headers
   120  }
   121  
   122  func (s *DefaultSession) IDTokenClaims() *jwt.IDTokenClaims {
   123  	if s.Claims == nil {
   124  		s.Claims = &jwt.IDTokenClaims{}
   125  	}
   126  	return s.Claims
   127  }
   128  
   129  type DefaultStrategy struct {
   130  	jwt.JWTStrategy
   131  
   132  	Expiry time.Duration
   133  	Issuer string
   134  
   135  	MinParameterEntropy int
   136  }
   137  
   138  func (h DefaultStrategy) GenerateIDToken(ctx context.Context, requester fosite.Requester) (token string, err error) {
   139  	if h.Expiry == 0 {
   140  		h.Expiry = defaultExpiryTime
   141  	}
   142  
   143  	sess, ok := requester.GetSession().(Session)
   144  	if !ok {
   145  		return "", errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to generate id token because session must be of type fosite/handler/openid.Session."))
   146  	}
   147  
   148  	claims := sess.IDTokenClaims()
   149  	if claims.Subject == "" {
   150  		return "", errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to generate id token because subject is an empty string."))
   151  	}
   152  
   153  	if requester.GetRequestForm().Get("grant_type") != "refresh_token" {
   154  		maxAge, err := strconv.ParseInt(requester.GetRequestForm().Get("max_age"), 10, 64)
   155  		if err != nil {
   156  			maxAge = 0
   157  		}
   158  
   159  		// Adds a bit of wiggle room for timing issues
   160  		if claims.AuthTime.After(time.Now().UTC().Add(time.Second * 5)) {
   161  			return "", errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to validate OpenID Connect request because authentication time is in the future."))
   162  		}
   163  
   164  		if maxAge > 0 {
   165  			if claims.AuthTime.IsZero() {
   166  				return "", errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to generate id token because authentication time claim is required when max_age is set."))
   167  			} else if claims.RequestedAt.IsZero() {
   168  				return "", errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to generate id token because requested at claim is required when max_age is set."))
   169  			} else if claims.AuthTime.Add(time.Second * time.Duration(maxAge)).Before(claims.RequestedAt) {
   170  				return "", errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to generate id token because authentication time does not satisfy max_age time."))
   171  			}
   172  		}
   173  
   174  		prompt := requester.GetRequestForm().Get("prompt")
   175  		if prompt != "" {
   176  			if claims.AuthTime.IsZero() {
   177  				return "", errorsx.WithStack(fosite.ErrServerError.WithDebug("Unable to determine validity of prompt parameter because auth_time is missing in id token claims."))
   178  			}
   179  		}
   180  
   181  		switch prompt {
   182  		case "none":
   183  			if !claims.AuthTime.Equal(claims.RequestedAt) && claims.AuthTime.After(claims.RequestedAt) {
   184  				return "", errorsx.WithStack(fosite.ErrServerError.
   185  					WithDebugf("Failed to generate id token 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))
   186  			}
   187  		case "login":
   188  			if !claims.AuthTime.Equal(claims.RequestedAt) && claims.AuthTime.Before(claims.RequestedAt) {
   189  				return "", errorsx.WithStack(fosite.ErrServerError.
   190  					WithDebugf("Failed to generate id token 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))
   191  			}
   192  		}
   193  
   194  		// If acr_values was requested but no acr value was provided in the ID token, fall back to level 0 which means least
   195  		// confidence in authentication.
   196  		if requester.GetRequestForm().Get("acr_values") != "" && claims.AuthenticationContextClassReference == "" {
   197  			claims.AuthenticationContextClassReference = "0"
   198  		}
   199  
   200  		if tokenHintString := requester.GetRequestForm().Get("id_token_hint"); tokenHintString != "" {
   201  			tokenHint, err := h.JWTStrategy.Decode(ctx, tokenHintString)
   202  			var ve *jwt.ValidationError
   203  			if errors.As(err, &ve) && ve.Has(jwt.ValidationErrorExpired) {
   204  				// Expired ID Tokens are allowed as values to id_token_hint
   205  			} else if err != nil {
   206  				return "", errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebugf("Unable to decode id token from 'id_token_hint' parameter because %s.", err.Error()))
   207  			}
   208  
   209  			if hintSub, _ := tokenHint.Claims["sub"].(string); hintSub == "" {
   210  				return "", errorsx.WithStack(fosite.ErrServerError.WithDebug("Provided id token from 'id_token_hint' does not have a subject."))
   211  			} else if hintSub != claims.Subject {
   212  				return "", errorsx.WithStack(fosite.ErrServerError.WithDebug("Subject from authorization mismatches id token subject from 'id_token_hint'."))
   213  			}
   214  		}
   215  	}
   216  
   217  	if claims.ExpiresAt.IsZero() {
   218  		claims.ExpiresAt = time.Now().UTC().Add(h.Expiry)
   219  	}
   220  
   221  	if claims.ExpiresAt.Before(time.Now().UTC()) {
   222  		return "", errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to generate id token because expiry claim can not be in the past."))
   223  	}
   224  
   225  	if claims.AuthTime.IsZero() {
   226  		claims.AuthTime = time.Now().Truncate(time.Second).UTC()
   227  	}
   228  
   229  	if claims.Issuer == "" {
   230  		claims.Issuer = h.Issuer
   231  	}
   232  
   233  	// OPTIONAL. String value used to associate a Client session with an ID Token, and to mitigate replay attacks.
   234  	if nonce := requester.GetRequestForm().Get("nonce"); len(nonce) == 0 {
   235  	} else if len(nonce) > 0 && len(nonce) < h.MinParameterEntropy {
   236  		// We're assuming that using less then, by default, 8 characters for the state can not be considered "unguessable"
   237  		return "", errorsx.WithStack(fosite.ErrInsufficientEntropy.WithHintf("Parameter 'nonce' is set but does not satisfy the minimum entropy of %d characters.", h.MinParameterEntropy))
   238  	} else if len(nonce) > 0 {
   239  		claims.Nonce = nonce
   240  	}
   241  
   242  	claims.Audience = stringslice.Unique(append(claims.Audience, requester.GetClient().GetID()))
   243  	claims.IssuedAt = time.Now().UTC()
   244  
   245  	token, _, err = h.JWTStrategy.Generate(ctx, claims.ToMapClaims(), sess.IDTokenHeaders())
   246  	return token, err
   247  }
   248  

View as plain text