Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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