...

Source file src/edge-infra.dev/pkg/edge/iam/barcode/flow.go

Documentation: edge-infra.dev/pkg/edge/iam/barcode

     1  package barcode
     2  
     3  import (
     4  	"context"
     5  	"html"
     6  	"time"
     7  
     8  	"github.com/ory/fosite"
     9  	"github.com/ory/fosite/handler/oauth2"
    10  	"github.com/ory/fosite/handler/openid"
    11  	"github.com/ory/fosite/token/hmac"
    12  	"github.com/ory/x/errorsx"
    13  
    14  	"edge-infra.dev/pkg/edge/iam/config"
    15  	"edge-infra.dev/pkg/edge/iam/device"
    16  	"edge-infra.dev/pkg/edge/iam/profile"
    17  	"edge-infra.dev/pkg/edge/iam/prometheus"
    18  	"edge-infra.dev/pkg/edge/iam/session"
    19  	"edge-infra.dev/pkg/edge/iam/util"
    20  
    21  	iamErrors "edge-infra.dev/pkg/edge/iam/errors"
    22  )
    23  
    24  // GrantHandler exchanges a barcode for an accesstoken
    25  type GrantHandler struct {
    26  	// barcode
    27  	BarcodeStrategy   *OpaqueStrategy
    28  	SignedStrategy    *SignedStrategy
    29  	LoginHintStrategy *hmac.HMACStrategy
    30  	BarcodeStorage    Storage
    31  
    32  	// access token
    33  	AccessTokenStrategy oauth2.AccessTokenStrategy
    34  	ScopeStrategy       fosite.ScopeStrategy
    35  	AccessTokenStorage  oauth2.AccessTokenStorage
    36  	ProfileStorage      profile.Storage
    37  	DeviceStorage       device.Storage
    38  	LoginSessionStorage session.LoginSessionStorage
    39  	ProfileTTL          time.Duration
    40  	BarcodeLength       uint8
    41  
    42  	// refresh token
    43  	RefreshTokenStrategy oauth2.RefreshTokenStrategy
    44  	RefreshTokenStorage  oauth2.RefreshTokenStorage
    45  	RefreshTokenScopes   []string
    46  	RevocationStorage    oauth2.TokenRevocationStorage
    47  
    48  	// id token
    49  	IDTokenHandleHelper *openid.IDTokenHandleHelper
    50  
    51  	// Custom metrics
    52  	Metrics *prometheus.Metrics
    53  }
    54  
    55  // can handle 'barcode' grant type
    56  func (h *GrantHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool {
    57  	return requester.GetGrantTypes().ExactOne("barcode")
    58  }
    59  
    60  // require client authentication
    61  func (h *GrantHandler) CanSkipClientAuth(_ fosite.AccessRequester) bool {
    62  	return false
    63  }
    64  
    65  // try to exchange barcode for subject
    66  func (h *GrantHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error {
    67  	// TODO:
    68  	// replace 7 with BarcodeInvalid after we migrated/created constants
    69  
    70  	// make sure we only handle 'barcode' flow
    71  	if !requester.GetGrantTypes().ExactOne("barcode") {
    72  		return errorsx.WithStack(fosite.ErrUnknownRequest)
    73  	}
    74  	h.Metrics.IncHTTPRequestsTotal(signInBarcode)
    75  
    76  	// get the client making the request
    77  	client := requester.GetClient()
    78  	s := session.FromRequester(requester)
    79  
    80  	// make sure its a private client
    81  	if client.IsPublic() {
    82  		hint := "The OAuth 2.0 Client is marked as public and is thus not allowed to use authorization grant 'barcode'."
    83  		return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint(hint))
    84  	}
    85  
    86  	// make sure the client is allowed
    87  	if !client.GetGrantTypes().Has("barcode") {
    88  		hint := "The OAuth 2.0 Client is not allowed to use authorization grant 'barcode'."
    89  		return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHint(hint))
    90  	}
    91  
    92  	// we're only working with first class citizens at the moment, so give them what they want, but only if originally part of their client manifest.
    93  	for _, scope := range requester.GetRequestedScopes() {
    94  		if !h.ScopeStrategy(client.GetScopes(), scope) {
    95  			return errorsx.WithStack(fosite.ErrInvalidScope.WithHintf("The OAuth 2.0 Client is not allowed to request scope '%s'.", scope))
    96  		}
    97  		requester.GrantScope(scope)
    98  	}
    99  
   100  	// try to grab the barcode from request
   101  	barcode := requester.GetRequestForm().Get("barcode")
   102  	if barcode == "" {
   103  		// I feel like this is the user error
   104  		h.Metrics.IncSignInRequestsTotal(signInBarcode, util.Failed)
   105  		hint := "The barcode parameter is missing"
   106  		return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHint(hint))
   107  	}
   108  
   109  	clientID := client.GetID()
   110  	var userProfile *profile.Profile
   111  	var err error
   112  
   113  	// Check for prefix before proceeding with further checks
   114  	if barcode[:len(config.BarcodePrefix())] != config.BarcodePrefix() {
   115  		h.Metrics.IncSignInRequestsTotal(signInBarcode, util.Failed)
   116  		return errorsx.WithStack(fosite.ErrRequestUnauthorized.
   117  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrUnrecognisedBarcode)))
   118  	}
   119  
   120  	if h.is128ABarcode(barcode) {
   121  		userProfile, err = h.handle128AScan(ctx, s, barcode, clientID)
   122  		if err != nil {
   123  			return err
   124  		}
   125  	} else {
   126  		userProfile, err = h.handleEBCScan(ctx, s, barcode, clientID)
   127  		if err != nil {
   128  			return err
   129  		}
   130  	}
   131  
   132  	for _, audience := range requester.GetGrantedAudience() {
   133  		requester.GrantAudience(audience)
   134  	}
   135  
   136  	// if userProfile.Organization != config.OrganizationID() {
   137  	// 	return fmt.Errorf("invalid user")
   138  	// }
   139  
   140  	s.SetOrg(userProfile.Organization)
   141  	s.SetRls(userProfile.Roles)
   142  
   143  	// add your profile info
   144  	s.SetGivenName(userProfile.GivenName)
   145  	s.SetFamilyName(userProfile.FamilyName)
   146  	s.SetAge(userProfile.Age)
   147  	s.SetDeviceLogin(userProfile.DeviceLogin)
   148  	s.SetFullName(userProfile.FullName)
   149  	s.SetEmail(userProfile.Email)
   150  	if userProfile.Address != nil {
   151  		s.SetAddress(userProfile.Address.ToMap())
   152  	}
   153  
   154  	// and set expiration
   155  	s.AsFosite().SetExpiresAt(fosite.AccessToken, accessExpiresAt())
   156  	s.AsFosite().SetExpiresAt(fosite.RefreshToken, refreshExpiresAt())
   157  	return nil
   158  }
   159  
   160  func hasValidRefreshToken(token string) error {
   161  	// expected to check only device login, as this piece of code is only hit when device login is enabled.
   162  	if util.IsDeviceLoginAvailable() {
   163  		return device.ExchangeRefreshToken(token)
   164  	}
   165  	return device.IsRefreshTokenValid(token)
   166  }
   167  
   168  func (h *GrantHandler) handle128AScan(ctx context.Context, s session.Session, barcode, clientID string) (*profile.Profile, error) {
   169  	// let's check the length of the barcode
   170  	if len(barcode) != int(h.BarcodeLength) {
   171  		h.Metrics.IncSignInRequestsTotal(signInBarcode, util.Failed)
   172  		return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   173  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrUnrecognisedBarcode)))
   174  	}
   175  
   176  	// try and get it from storage
   177  	key := h.BarcodeStrategy.GetKey(barcode)
   178  	barcodeData, err := h.BarcodeStorage.GetBarcode(ctx, key)
   179  	if err != nil {
   180  		return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   181  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrUnrecognisedBarcode)))
   182  	}
   183  
   184  	// place the identity in the session
   185  	userProfile, err := h.ProfileStorage.GetIdentityProfile(ctx, barcodeData.Subject)
   186  	if err != nil {
   187  		return nil, errorsx.WithStack(fosite.ErrServerError.
   188  			WithWrap(err).
   189  			WithDebug("storage error"))
   190  	}
   191  
   192  	if userProfile == nil {
   193  		return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   194  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrUnrecognisedBarcode)))
   195  	}
   196  
   197  	s.SetSubject(userProfile.Subject)
   198  
   199  	// may be, we need to abstract and ensure refresh token ends up in userProfile
   200  	// the storage takes care of fetching from appropriate documents depending on type of login method
   201  	// and puts in user profile for refreshing here.
   202  	// the refresh method should also be available on cloud service used
   203  	// which is injected here for refreshing.
   204  	// or ensure, we understand this in our v3 :).
   205  	if config.DeviceLoginEnabled() {
   206  		if err := h.verifyDeviceAuth(ctx, s, userProfile, clientID); err != nil {
   207  			// treat a barcode auth, without valid cloud refresh token as an expired barcode.
   208  			return userProfile, err
   209  		}
   210  	} else {
   211  		if err := h.verifyProfile(s, userProfile, clientID); err != nil {
   212  			return userProfile, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   213  				WithWrap(err).WithDebug("error verifying profile"))
   214  		}
   215  	}
   216  
   217  	currentBarcodeKey, err := h.BarcodeStorage.GetBarcodeUser(ctx, barcodeData.Subject)
   218  	if err != nil {
   219  		// remember that barcode = key + secret
   220  		// some error occurred while fetching the barcode-user value i.e, barcode key
   221  		return nil, errorsx.WithStack(fosite.ErrServerError.WithHint(err.Error()))
   222  	}
   223  
   224  	// now, let's check if the current scanned barcode key and last printed barcode key equals or not?
   225  	if currentBarcodeKey != key {
   226  		h.Metrics.IncSignInRequestsTotal(signInBarcode, util.Failed)
   227  		return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   228  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrExpiredBarcode)))
   229  	}
   230  
   231  	// verify it - here we now the subject,
   232  	if err := h.BarcodeStrategy.Verify(barcode, barcodeData); err != nil {
   233  		h.Metrics.IncSignInRequestsTotal(signInBarcode, util.Failed)
   234  		if err == iamErrors.ErrExpiredBarcode {
   235  			return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   236  				WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrExpiredBarcode)))
   237  		}
   238  		return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   239  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrUnrecognisedBarcode)))
   240  	}
   241  	return userProfile, nil
   242  }
   243  
   244  func (h *GrantHandler) verifyDeviceAuth(ctx context.Context, s session.Session, userProfile *profile.Profile, clientID string) error {
   245  	deviceLogin := userProfile.DeviceLogin
   246  	acc, err := h.DeviceStorage.GetDeviceAccount(ctx, deviceLogin)
   247  	if err != nil {
   248  		return errorsx.WithStack(fosite.ErrServerError.
   249  			WithWrap(err).
   250  			WithDebug("error fetching device account from storage"))
   251  	}
   252  	if acc == nil {
   253  		return errorsx.WithStack(fosite.ErrRequestUnauthorized.
   254  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrUnrecognisedBarcode)).
   255  			WithDebug("no such device account"))
   256  	}
   257  	// todo: inject into struct, may be via cloud svc struct, so we can use the same signature for BSL/device/okta etc.
   258  	err = hasValidRefreshToken(acc.RefreshToken)
   259  	if err != nil {
   260  		return errorsx.WithStack(fosite.ErrRequestUnauthorized.
   261  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrExpiredBarcode)))
   262  	}
   263  	return nil
   264  }
   265  
   266  func (h *GrantHandler) verifyProfile(s session.Session, userProfile *profile.Profile, clientID string) error {
   267  	verify, err := userProfile.RequireVerification(h.BarcodeStorage.IsOffline())
   268  	if err != nil {
   269  		return errorsx.WithStack(fosite.ErrServerError.
   270  			WithWrap(err).
   271  			WithDebug("profile error"))
   272  	}
   273  	if verify {
   274  		challenge, signature, err := h.LoginHintStrategy.Generate()
   275  		if err != nil {
   276  			return errorsx.WithStack(fosite.ErrServerError.WithHint(err.Error()))
   277  		}
   278  		s.SetChallenge(challenge)
   279  
   280  		err = h.LoginSessionStorage.SetLoginSession(signature, &session.LoginSession{
   281  			Subject:      userProfile.Subject,
   282  			Reason:       1, //ProfileDataExpired is 1, hardcoding until reason enum is moved from identity to constants
   283  			Active:       true,
   284  			ClientID:     clientID,
   285  			LoginOptions: session.LoginOptions{},
   286  		})
   287  
   288  		if err != nil {
   289  			return errorsx.WithStack(fosite.ErrServerError.WithHint(err.Error()))
   290  		}
   291  		return iamErrors.ErrLoginRequired
   292  	}
   293  	return nil
   294  }
   295  
   296  func (h *GrantHandler) handleEBCScan(ctx context.Context, s session.Session, barcode, clientID string) (*profile.Profile, error) {
   297  	payload, err := h.SignedStrategy.Verify(barcode)
   298  	// do we have a valid emergency barcode?
   299  	if err == iamErrors.ErrUnrecognisedBarcode {
   300  		return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   301  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrUnrecognisedBarcode)))
   302  	} else if err == iamErrors.ErrExpiredEBC {
   303  		return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   304  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrExpiredBarcode)))
   305  	} else if err != nil {
   306  		return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   307  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrUnrecognisedBarcode)))
   308  	}
   309  
   310  	// if we can reach BOTH:
   311  	// 1) couchDB on the control plane
   312  	// 2) Device Login/BSL/Okta (based on config)
   313  	// Then, we decline the EBC as it should be used only for offline scenario...
   314  	if !h.BarcodeStorage.IsOffline() && util.IsCloudLoginAvailable() {
   315  		return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   316  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrInvalidEBCUsage)))
   317  	}
   318  
   319  	subject, err := h.ProfileStorage.GetSubjectFromAlias(ctx, payload.Subject)
   320  	if err != nil {
   321  		return nil, errorsx.WithStack(fosite.ErrServerError.
   322  			WithWrap(err).
   323  			WithDebug("storage error, missing subject for alias."))
   324  	}
   325  
   326  	userProfile, err := h.ProfileStorage.GetIdentityProfile(ctx, subject)
   327  	if err != nil {
   328  		return nil, errorsx.WithStack(fosite.ErrServerError.
   329  			WithWrap(err).
   330  			WithDebug("storage error"))
   331  	}
   332  
   333  	if userProfile == nil {
   334  		return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   335  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrUnrecognisedBarcode)))
   336  	}
   337  
   338  	if time.Unix(userProfile.LastUpdated, 0).Sub(time.Unix(payload.IssuedAt, 0)) > 0 {
   339  		return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.
   340  			WithWrap(h.invalidBarcodeScan(s, clientID, iamErrors.ErrExpiredBarcode)))
   341  	}
   342  
   343  	s.SetSubject(userProfile.Subject)
   344  
   345  	return userProfile, nil
   346  }
   347  
   348  //nolint:unparam
   349  func (h *GrantHandler) invalidBarcodeScan(s session.Session, clientID string, barcodeErr error) error {
   350  	challenge, signature, err := h.LoginHintStrategy.Generate()
   351  	if err != nil {
   352  		return errorsx.WithStack(fosite.ErrServerError.WithHint(err.Error()))
   353  	}
   354  
   355  	s.SetChallenge(challenge)
   356  
   357  	loginSession := &session.LoginSession{
   358  		Subject:  "", // don't expose the subject
   359  		Reason:   7,  // barcode invalid
   360  		Active:   true,
   361  		ClientID: clientID,
   362  		LoginOptions: session.LoginOptions{
   363  			ErrorMessage: html.EscapeString(barcodeErr.Error()),
   364  		},
   365  	}
   366  
   367  	err = h.LoginSessionStorage.SetLoginSession(signature, loginSession)
   368  
   369  	if err != nil {
   370  		return errorsx.WithStack(fosite.ErrServerError.WithHint(err.Error()))
   371  	}
   372  
   373  	return barcodeErr
   374  }
   375  
   376  // issue access and possibly refresh and id tokens
   377  func (h *GrantHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) (err error) {
   378  	// just making sure, I guess
   379  	if !h.CanHandleTokenEndpointRequest(requester) {
   380  		return errorsx.WithStack(fosite.ErrUnknownRequest)
   381  	}
   382  
   383  	// generate, store and populate the access token
   384  	if err = h.PopulateAccessToken(ctx, requester, responder); err != nil {
   385  		return err
   386  	}
   387  
   388  	// issue refresh token if offline scope is requested and client is allowed to
   389  	if h.CanIssueRefreshToken(requester) {
   390  		if err = h.PopulateRefreshToken(ctx, requester, responder); err != nil {
   391  			return err
   392  		}
   393  	}
   394  
   395  	// issue id token if openid scope is requested and client is allowed to
   396  	if h.CanIssueIDToken(requester) {
   397  		if err = h.PopulateIDToken(ctx, requester, responder); err != nil {
   398  			return err
   399  		}
   400  	}
   401  	h.Metrics.IncSignInRequestsTotal(signInBarcode, util.Succeeded)
   402  	return nil
   403  }
   404  
   405  func (h *GrantHandler) PopulateIDToken(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error {
   406  	// get our session from the request
   407  	s := session.FromRequester(requester)
   408  
   409  	// we require a subject in the session in order to issue the ID token
   410  	claims := s.AsOpenID().Claims
   411  	if claims.Subject == "" {
   412  		return errorsx.WithStack(fosite.ErrServerError.WithDebug("Failed to generate id token because subject is an empty string."))
   413  	}
   414  
   415  	// we're linking the ID token to the issued accesstoken - we verify it at one point
   416  	claims.AccessTokenHash = h.IDTokenHandleHelper.GetAccessTokenHash(ctx, requester, responder)
   417  
   418  	// generate the ID token and populate the id_token field on the response
   419  	return h.IDTokenHandleHelper.IssueExplicitIDToken(ctx, requester, responder)
   420  }
   421  
   422  // PopulateAccessToken generates, stores and populates the response with the access token
   423  func (h *GrantHandler) PopulateAccessToken(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error {
   424  	// generate
   425  	accessToken, accessSignature, err := h.AccessTokenStrategy.GenerateAccessToken(ctx, requester)
   426  	if err != nil {
   427  		return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error()))
   428  	}
   429  
   430  	// store
   431  	if err = h.AccessTokenStorage.CreateAccessTokenSession(ctx, accessSignature, requester.Sanitize([]string{})); err != nil {
   432  		return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error()))
   433  	}
   434  
   435  	// respond
   436  	s := session.FromRequester(requester)
   437  	responder.SetAccessToken(accessToken)
   438  	responder.SetTokenType("bearer")
   439  	expiresAt := s.AsFosite().GetExpiresAt(fosite.AccessToken)
   440  	responder.SetExpiresIn(time.Until(expiresAt))
   441  
   442  	return nil
   443  }
   444  
   445  func (h *GrantHandler) CanIssueRefreshToken(requester fosite.Requester) bool {
   446  	// Require one of the refresh token scopes, if set.
   447  	if len(h.RefreshTokenScopes) > 0 && !requester.GetGrantedScopes().HasOneOf(h.RefreshTokenScopes...) {
   448  		return false
   449  	}
   450  	// Do not issue a refresh token to clients that cannot use the refresh token grant type.
   451  	if !requester.GetClient().GetGrantTypes().Has("refresh_token") {
   452  		return false
   453  	}
   454  	return true
   455  }
   456  
   457  // PopulateRefreshToken generates, stores and populates the response with the refresh token
   458  func (h *GrantHandler) PopulateRefreshToken(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error {
   459  	// generate
   460  	refreshToken, refreshSignature, err := h.RefreshTokenStrategy.GenerateRefreshToken(ctx, requester)
   461  	if err != nil {
   462  		return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error()))
   463  	}
   464  
   465  	// store
   466  	if err = h.RefreshTokenStorage.CreateRefreshTokenSession(ctx, refreshSignature, requester.Sanitize([]string{})); err != nil {
   467  		return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error()))
   468  	}
   469  
   470  	// populate
   471  	responder.SetExtra("refresh_token", refreshToken)
   472  
   473  	return nil
   474  }
   475  
   476  func (h *GrantHandler) CanIssueIDToken(requester fosite.Requester) bool {
   477  	// Require one of the refresh token scopes, if set.
   478  	if len(h.RefreshTokenScopes) > 0 && !requester.GetGrantedScopes().HasOneOf("openid") {
   479  		return false
   480  	}
   481  	// Do not issue a refresh token to clients that cannot use the refresh token grant type.
   482  	if !requester.GetClient().GetGrantTypes().Has("authorization_code") {
   483  		return false
   484  	}
   485  	return true
   486  }
   487  
   488  func (h *GrantHandler) is128ABarcode(barcode string) bool {
   489  	// placeholder for adding more validations
   490  	return len(barcode) <= int(h.BarcodeLength)
   491  }
   492  

View as plain text