package barcode import ( "context" "errors" "fmt" "time" "github.com/ory/fosite" "github.com/ory/fosite/token/hmac" "github.com/ory/x/errorsx" "golang.org/x/crypto/bcrypt" "edge-infra.dev/pkg/edge/iam/config" iamErrors "edge-infra.dev/pkg/edge/iam/errors" "edge-infra.dev/pkg/edge/iam/prometheus" "edge-infra.dev/pkg/edge/iam/session" "edge-infra.dev/pkg/edge/iam/util" ) // CodeGrantHandler handles the "barcode_code" grant where you exchange the code // received on the printBarcodeUrl for an actual barcode type CodeGrantHandler struct { BarcodeStrategy *OpaqueStrategy SignedStrategy *SignedStrategy BarcodeStorage Storage BarcodeCodeHMACStrategy *hmac.HMACStrategy // Custom metrics Metrics *prometheus.Metrics } // CanHandleTokenEndpointRequest makes sure we handle 'barcode_code' grant type func (h *CodeGrantHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool { return requester.GetGrantTypes().ExactOne("barcode_code") } // CanSkipClientAuth makes sure we only allow authenticated clients func (h *CodeGrantHandler) CanSkipClientAuth(_ fosite.AccessRequester) bool { return false } // HandleTokenEndpointRequest validates the code and tries to prevent any intrusion attempts func (h *CodeGrantHandler) HandleTokenEndpointRequest(ctx context.Context, requester fosite.AccessRequester) error { // only on grant type "barcode_code" if !h.CanHandleTokenEndpointRequest(requester) { return errorsx.WithStack(fosite.ErrUnknownRequest) } h.Metrics.IncHTTPRequestsTotal(signUpBarcode) // get the client that made the request client := requester.GetClient() // make sure the client is allowed if !client.GetGrantTypes().Has("barcode") { return errorsx.WithStack(fosite.ErrUnauthorizedClient. WithHint("The OAuth 2.0 Client is not allowed to use authorization grant 'barcode'.")) } // and that auth is required if client.IsPublic() { return errorsx.WithStack(fosite.ErrInvalidGrant. WithHint("The OAuth 2.0 Client is marked as public and is thus not allowed to use authorization grant 'barcode'.")) } // take the code from the request code := requester.GetRequestForm().Get("code") signature := h.BarcodeCodeHMACStrategy.Signature(code) // and try to get it from storage codeData, err := h.BarcodeStorage.GetBarcodeCode(ctx, signature) if err != nil { // let's check if this error is because of used barcode code? if errors.Is(err, iamErrors.ErrUsedBarcodeCode) { return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint(err.Error())) } // this is internal server error return errorsx.WithStack(fosite.ErrServerError.WithHint(err.Error())) } // validate issued barcode-code with the signature and secret used to generate one. err = h.BarcodeCodeHMACStrategy.Validate(code) if err != nil { return errorsx.WithStack(fosite.ErrInvalidGrant.WithWrap(err).WithDebug(err.Error())) } // code is being exchanged after expiry expires := codeData.CreatedAt.UTC().Add(config.GetBarcodeCodeLifespan()) if expires.Before(time.Now().UTC()) { // do not continue and delete this `barcode code`, in the future it might be better // to mark this code as used and if they try to use again we might // have a tamper attempt. We can maybe delete active barcode or // request additional verification on any next sign-in for that user err := h.BarcodeStorage.DeleteBarcodeCode(ctx, code) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } // error is NOT nil here! return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("received expired/invalid code")) } // in populate token we create the barcode and we should mark the code as used // if the code has been used already, revoke the tokens that belong to it // https://github.com/ory/fosite/blob/7edf673f20aece260f9ba677a07086c48835fba8/handler/oauth2/flow_authorize_code_token.go#L59 // you need to be the client that asked for this code if codeData.ClientID != client.GetID() { // delete barcode code, we are compromised err := h.BarcodeStorage.DeleteBarcodeCode(ctx, code) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error())) } return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("you are not the right client")) } // set the subject on the session for use in populate subject := codeData.Subject session.FromRequester(requester).SetSubject(subject) return nil } func (h *CodeGrantHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) (err error) { // only on barcode grant if !h.CanHandleTokenEndpointRequest(requester) { return errorsx.WithStack(fosite.ErrUnknownRequest) } signature := h.BarcodeCodeHMACStrategy.Signature(requester.GetRequestForm().Get("code")) barCodeCode, err := h.BarcodeStorage.GetBarcodeCode(ctx, signature) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithHint(err.Error())) } var barcode string switch barCodeCode.Type { case "qr": barcode, err = h.SignedStrategy.Generate(barCodeCode.Subject, barCodeCode.IssuedBy) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithHint("failed to generate encoded token for QR barcode")) } case "128A": barcode, err = h.BarcodeStrategy.Generate() if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithHint("failed to generate encoded token for 128A barcode")) } subject := session.FromRequester(requester).GetSubject() key := h.BarcodeStrategy.GetKey(barcode) // Stores the barcode key, associating it with the challenge if err = h.BarcodeStorage.CreateBarcodeKey(ctx, barCodeCode.Challenge, key); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithHintf("failed to create barcode key: %v", err.Error())) } // Creating a barcode user if it doesnt already exit. // Or else we will get a 500 error trying to scan a barcode which didnt complete continuation properly if _, err := h.BarcodeStorage.GetBarcodeUser(ctx, subject); err != nil { if err = h.BarcodeStorage.CreateBarcodeUser(ctx, subject, ""); err != nil { return errorsx.WithStack(fosite.ErrServerError.WithHintf("failed to create barcode user: %v", err.Error())) } } barcodePrefixLen := uint8(len(config.BarcodePrefix())) /* #nosec G115 value from ENV and ENV is considered trusted */ secret := barcode[barcodePrefixLen+config.GetBarcodeKeyLength():] hash, _ := bcrypt.GenerateFromPassword([]byte(secret), config.BcryptCost()) if err := h.BarcodeStorage.CreateBarcode(ctx, key, string(hash), subject); err != nil { return fmt.Errorf("failed to create barcode 128A: %w", err) } } // mark the barcode code as used err = h.BarcodeStorage.InvalidateBarcodeCode(ctx, signature, barCodeCode.Subject, barCodeCode.Subject, barCodeCode.ClientID, barCodeCode.Type, barCodeCode.Challenge) if err != nil { return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error()).WithHint("failed to invalidate barcode code")) } // return the barcode as access token with its own type responder.SetAccessToken(barcode) responder.SetTokenType(barCodeCode.Type) h.Metrics.IncSignUpRequestsTotal(signUpBarcode, util.Succeeded) return nil }