Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,9 @@ func buildAPIDependencies(
roleService := role.NewService(roleRepository, relationService, permissionService, auditRecordRepository, cfg.App.PAT.DeniedPermissionsSet())
policyService := policy.NewService(policyPGRepository, relationService, roleService)
userService := user.NewService(userRepository, relationService, policyService, roleService)
patValidator := userpat.NewValidator(logger, userPATRepo, cfg.App.PAT)
authnService := authenticate.NewService(logger, cfg.App.Authentication,
postgres.NewFlowRepository(logger, dbc), mailDialer, tokenService, sessionService, userService, serviceUserService, webAuthConfig)
postgres.NewFlowRepository(logger, dbc), mailDialer, tokenService, sessionService, userService, serviceUserService, webAuthConfig, patValidator)
groupService := group.NewService(groupRepository, relationService, authnService, policyService)
organizationService := organization.NewService(organizationRepository, relationService, userService,
authnService, policyService, preferenceService, auditRecordRepository)
Expand Down
9 changes: 7 additions & 2 deletions core/authenticate/authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/raystack/frontier/core/serviceuser"
"github.com/raystack/frontier/core/user"
pat "github.com/raystack/frontier/core/userpat/models"

"github.com/raystack/frontier/pkg/metadata"

Expand Down Expand Up @@ -42,6 +43,8 @@ const (
// ClientCredentialsClientAssertion is used to authenticate using client_id and client_secret
// that provides access token for the client
ClientCredentialsClientAssertion ClientAssertion = "client_credentials"
// PATClientAssertion is used to authenticate using Personal Access Token
PATClientAssertion ClientAssertion = "pat"
// PassthroughHeaderClientAssertion is used to authenticate using headers passed by the client
// this is non secure way of authenticating client in test environments
PassthroughHeaderClientAssertion ClientAssertion = "passthrough_header"
Expand All @@ -53,9 +56,10 @@ func (a ClientAssertion) String() string {

var APIAssertions = []ClientAssertion{
SessionClientAssertion,
PATClientAssertion,
AccessTokenClientAssertion,
OpaqueTokenClientAssertion,
JWTGrantClientAssertion,
OpaqueTokenClientAssertion,
// ClientCredentialsClientAssertion should be removed in future to avoid DDOS attacks on CPU
// and should only be allowed to be used get access token for the client
ClientCredentialsClientAssertion,
Expand Down Expand Up @@ -131,9 +135,10 @@ type Principal struct {
// ID is the unique identifier of principal
ID string
// Type is the namespace of principal
// E.g. app/user, app/serviceuser
// E.g. app/user, app/serviceuser, app/pat
Type string

User *user.User
ServiceUser *serviceuser.ServiceUser
PAT *pat.PAT
}
227 changes: 227 additions & 0 deletions core/authenticate/authenticators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package authenticate

import (
"context"
"encoding/base64"
"fmt"
"strings"

"github.com/lestrrat-go/jwx/v2/jwt"
frontiersession "github.com/raystack/frontier/core/authenticate/session"
"github.com/raystack/frontier/core/authenticate/token"
patErrors "github.com/raystack/frontier/core/userpat/errors"
"github.com/raystack/frontier/internal/bootstrap/schema"
"github.com/raystack/frontier/pkg/errors"
"github.com/raystack/frontier/pkg/utils"
)

// AuthenticatorFunc attempts to authenticate a request.
// Returns (Principal, nil) on success, errSkip if not applicable (try next),
// or any other error for a terminal authentication failure.
type AuthenticatorFunc func(ctx context.Context, s *Service) (Principal, error)

// authenticators maps each ClientAssertion to its authentication function.
var authenticators = map[ClientAssertion]AuthenticatorFunc{
SessionClientAssertion: authenticateWithSession,
PATClientAssertion: authenticateWithPAT,
AccessTokenClientAssertion: authenticateWithAccessToken,
JWTGrantClientAssertion: authenticateWithJWTGrant,
ClientCredentialsClientAssertion: authenticateWithClientCredentials,
OpaqueTokenClientAssertion: authenticateWithClientCredentials,
PassthroughHeaderClientAssertion: authenticateWithPassthroughHeader,
}

// authenticateWithSession extracts user from session cookie.
// Copied from original GetPrincipal session block.
func authenticateWithSession(ctx context.Context, s *Service) (Principal, error) {
session, err := s.sessionService.ExtractFromContext(ctx)
if err == nil && session.IsValid(s.Now()) && utils.IsValidUUID(session.UserID) {
// userID is a valid uuid
currentUser, err := s.userService.GetByID(ctx, session.UserID)
if err != nil {
s.log.Debug(fmt.Sprintf("unable to get session user by id: %v", err))
return Principal{}, err
}
return Principal{
ID: currentUser.ID,
Type: schema.UserPrincipal,
User: &currentUser,
}, nil
}
if err != nil && !errors.Is(err, frontiersession.ErrNoSession) {
s.log.Debug(fmt.Sprintf("unable to extract session from context: %v", err))
return Principal{}, err
}
return Principal{}, errSkip
}

// authenticateWithPAT validates a personal access token.
func authenticateWithPAT(ctx context.Context, s *Service) (Principal, error) {
value, ok := GetTokenFromContext(ctx)
if !ok {
return Principal{}, errSkip
}

pat, err := s.userPATService.Validate(ctx, value)
Comment thread
AmanGIT07 marked this conversation as resolved.
if err != nil {
if errors.Is(err, patErrors.ErrInvalidPAT) || errors.Is(err, patErrors.ErrDisabled) {
return Principal{}, errSkip
}
s.log.Debug("PAT validation failed", "err", err)
return Principal{}, err
}

// resolve the owning user so downstream handlers can access principal.User
currentUser, err := s.userService.GetByID(ctx, pat.UserID)
if err != nil {
s.log.Debug("failed to get PAT owner", "err", err)
return Principal{}, err
}

return Principal{
ID: pat.ID,
Type: schema.PATPrincipal,
PAT: &pat,
User: &currentUser,
}, nil
}

// authenticateWithAccessToken validates a Frontier-issued JWT access token.
// Copied from original GetPrincipal access token block.
func authenticateWithAccessToken(ctx context.Context, s *Service) (Principal, error) {
userToken, ok := GetTokenFromContext(ctx)
if !ok {
return Principal{}, errSkip
}

insecureJWT, err := jwt.ParseInsecure([]byte(userToken))
if err != nil {
// NOTE: in the original code, AccessToken and JWTGrant were in the same if-block,
// so JWT parse failure fell through to GetByJWT. With separate authenticators,
// errSkip is required to preserve that behavior.
s.log.Debug(fmt.Sprintf("unable to parse token: %v", err))
return Principal{}, errSkip
}

// check type of jwt
if genClaim, ok := insecureJWT.Get(token.GeneratedClaimKey); ok {
// jwt generated by frontier using public key
claimVal, ok := genClaim.(string)
if !ok || claimVal != token.GeneratedClaimValue {
s.log.Debug("generated claim value mismatch")
return Principal{}, errors.ErrUnauthenticated
}

// extract user from token if present as its created by frontier
userID, claims, err := s.internalTokenService.Parse(ctx, []byte(userToken))
if err != nil || !utils.IsValidUUID(userID) {
s.log.Debug("failed to parse as internal token ", "err", err)
return Principal{}, errors.ErrUnauthenticated
}

// userID is a valid uuid
if claims[token.SubTypeClaimsKey] == schema.ServiceUserPrincipal {
currentUser, err := s.serviceUserService.Get(ctx, userID)
if err != nil {
s.log.Debug("failed to get service user", "err", err)
return Principal{}, err
}
return Principal{
ID: currentUser.ID,
Type: schema.ServiceUserPrincipal,
ServiceUser: &currentUser,
}, nil
}

currentUser, err := s.userService.GetByID(ctx, userID)
if err != nil {
s.log.Debug("failed to get user", "err", err)
return Principal{}, err
}
return Principal{
ID: currentUser.ID,
Type: schema.UserPrincipal,
User: &currentUser,
}, nil
}

// NOTE: in the original code, a valid JWT without GeneratedClaimKey fell through to
// GetByJWT within the same if-block. errSkip preserves that behavior.
return Principal{}, errSkip
}

// authenticateWithJWTGrant validates a service user JWT grant token.
// Copied from original GetPrincipal jwt grant block.
func authenticateWithJWTGrant(ctx context.Context, s *Service) (Principal, error) {
userToken, ok := GetTokenFromContext(ctx)
if !ok {
return Principal{}, errSkip
}

serviceUser, err := s.serviceUserService.GetByJWT(ctx, userToken)
if err == nil {
return Principal{
ID: serviceUser.ID,
Type: schema.ServiceUserPrincipal,
ServiceUser: &serviceUser,
}, nil
}
s.log.Debug("failed to parse as user token ", "err", err)
return Principal{}, errors.ErrUnauthenticated
}

// authenticateWithClientCredentials validates client_id:client_secret credentials.
// Copied from original GetPrincipal client credentials block.
func authenticateWithClientCredentials(ctx context.Context, s *Service) (Principal, error) {
userSecretRaw, ok := GetSecretFromContext(ctx)
if !ok {
return Principal{}, errSkip
}

// verify client secret
userSecret, err := base64.StdEncoding.DecodeString(userSecretRaw)
if err != nil {
s.log.Debug("failed to decode user secret", "err", err)
return Principal{}, errors.ErrUnauthenticated
}
userSecretParts := strings.Split(string(userSecret), ":")
if len(userSecretParts) != 2 {
s.log.Debug("failed to parse user secret")
return Principal{}, errors.ErrUnauthenticated
}
clientID, clientSecret := userSecretParts[0], userSecretParts[1]

// extract user from secret if it's a service user
serviceUser, err := s.serviceUserService.GetBySecret(ctx, clientID, clientSecret)
if err == nil {
return Principal{
ID: serviceUser.ID,
Type: schema.ServiceUserPrincipal,
ServiceUser: &serviceUser,
}, nil
}
s.log.Debug("failed to authenticate with client credentials", "err", err)
return Principal{}, errors.ErrUnauthenticated
}

// authenticateWithPassthroughHeader extracts user from email header.
// Copied from original GetPrincipal passthrough block.
func authenticateWithPassthroughHeader(ctx context.Context, s *Service) (Principal, error) {
// check if header with user email is set
// TODO(kushsharma): this should ideally be deprecated
val, ok := GetEmailFromContext(ctx)
if !ok || len(val) == 0 {
return Principal{}, errSkip
}

currentUser, err := s.getOrCreateUser(ctx, strings.TrimSpace(val), strings.Split(val, "@")[0])
if err != nil {
s.log.Debug("failed to get user", "err", err)
return Principal{}, err
}
return Principal{
ID: currentUser.ID,
Type: schema.UserPrincipal,
User: &currentUser,
}, nil
}
3 changes: 3 additions & 0 deletions core/authenticate/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ import "errors"

var (
ErrInvalidID = errors.New("user id is invalid")

// errSkip signals that this authenticator doesn't apply to the request.
errSkip = errors.New("skip authenticator")
)
94 changes: 94 additions & 0 deletions core/authenticate/mocks/user_pat_service.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading