From 18496382d57ac8016affc9e062de0aaaf2f3c0ad Mon Sep 17 00:00:00 2001 From: aman Date: Sun, 5 Apr 2026 19:53:27 +0530 Subject: [PATCH 1/2] feat: add PAT identity to JWT claims for upstream scope enforcement --- core/authenticate/authenticators.go | 25 +++++++++++++++++++++++++ core/authenticate/service.go | 4 ++++ core/authenticate/token/service.go | 1 + core/userpat/validator.go | 6 ++++++ 4 files changed, 36 insertions(+) diff --git a/core/authenticate/authenticators.go b/core/authenticate/authenticators.go index 5dd361d36..a99651d06 100644 --- a/core/authenticate/authenticators.go +++ b/core/authenticate/authenticators.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "fmt" "strings" + "time" "github.com/lestrrat-go/jwx/v2/jwt" frontiersession "github.com/raystack/frontier/core/authenticate/session" @@ -133,6 +134,30 @@ func authenticateWithAccessToken(ctx context.Context, s *Service) (Principal, er }, nil } + if claims[token.SubTypeClaimsKey] == schema.PATPrincipal { + patID := userID // sub = PAT ID + pat, err := s.userPATService.GetByID(ctx, patID) + if err != nil { + s.log.Debug("failed to get PAT", "err", err) + return Principal{}, errors.ErrUnauthenticated + } + if pat.ExpiresAt.Before(time.Now()) { + s.log.Debug("PAT has expired", "pat_id", patID) + return Principal{}, errors.ErrUnauthenticated + } + currentUser, err := s.userService.GetByID(ctx, pat.UserID) + if err != nil { + s.log.Debug("failed to get PAT owner", "err", err) + return Principal{}, errors.ErrUnauthenticated + } + return Principal{ + ID: pat.ID, + Type: schema.PATPrincipal, + PAT: &pat, + User: ¤tUser, + }, nil + } + currentUser, err := s.userService.GetByID(ctx, userID) if err != nil { s.log.Debug("failed to get user", "err", err) diff --git a/core/authenticate/service.go b/core/authenticate/service.go index 45a7754be..f963d3302 100644 --- a/core/authenticate/service.go +++ b/core/authenticate/service.go @@ -90,6 +90,7 @@ type TokenService interface { type UserPATService interface { Validate(ctx context.Context, value string) (patModels.PAT, error) + GetByID(ctx context.Context, id string) (patModels.PAT, error) } type Service struct { @@ -691,6 +692,9 @@ func (s Service) BuildToken(ctx context.Context, principal Principal, metadata m if principal.Type == schema.UserPrincipal && s.config.Token.Claims.AddUserEmailClaim { metadata[token.SubEmailClaimsKey] = principal.User.Email } + if principal.Type == schema.PATPrincipal && principal.User != nil { + metadata[token.UserIDClaimKey] = principal.User.ID + } return s.internalTokenService.Build(principal.ID, metadata) } diff --git a/core/authenticate/token/service.go b/core/authenticate/token/service.go index 6a568614d..dcd0fc418 100644 --- a/core/authenticate/token/service.go +++ b/core/authenticate/token/service.go @@ -24,6 +24,7 @@ const ( SubTypeClaimsKey = "sub_type" SubEmailClaimsKey = "email" SessionIDClaimKey = "sid" + UserIDClaimKey = "user_id" ) type Service struct { diff --git a/core/userpat/validator.go b/core/userpat/validator.go index f62d3972d..39c51a3af 100644 --- a/core/userpat/validator.go +++ b/core/userpat/validator.go @@ -67,3 +67,9 @@ func (v *Validator) Validate(ctx context.Context, value string) (models.PAT, err return pat, nil } + +// GetByID retrieves a PAT by ID. Used by the access token authenticator +// to reconstruct the PAT principal from JWT claims. +func (v *Validator) GetByID(ctx context.Context, id string) (models.PAT, error) { + return v.repo.GetByID(ctx, id) +} From bf9845b0181765801e79c6243c076ae801d5df8b Mon Sep 17 00:00:00 2001 From: aman Date: Mon, 6 Apr 2026 12:42:47 +0530 Subject: [PATCH 2/2] refactor: use mocked clock for PAT expiration check --- core/authenticate/authenticators.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/authenticate/authenticators.go b/core/authenticate/authenticators.go index a99651d06..0050ff71a 100644 --- a/core/authenticate/authenticators.go +++ b/core/authenticate/authenticators.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "fmt" "strings" - "time" "github.com/lestrrat-go/jwx/v2/jwt" frontiersession "github.com/raystack/frontier/core/authenticate/session" @@ -141,7 +140,7 @@ func authenticateWithAccessToken(ctx context.Context, s *Service) (Principal, er s.log.Debug("failed to get PAT", "err", err) return Principal{}, errors.ErrUnauthenticated } - if pat.ExpiresAt.Before(time.Now()) { + if pat.ExpiresAt.Before(s.Now()) { s.log.Debug("PAT has expired", "pat_id", patID) return Principal{}, errors.ErrUnauthenticated }