Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
395 lines
8.7 KiB
Go
395 lines
8.7 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cortezaproject/corteza/server/pkg/payload"
|
|
"github.com/go-oauth2/oauth2/v4"
|
|
"github.com/go-oauth2/oauth2/v4/generates"
|
|
"github.com/go-oauth2/oauth2/v4/models"
|
|
"github.com/lestrrat-go/jwx/jwa"
|
|
"github.com/lestrrat-go/jwx/jwk"
|
|
"github.com/lestrrat-go/jwx/jwt"
|
|
"github.com/spf13/cast"
|
|
)
|
|
|
|
type (
|
|
tokenIssuer struct {
|
|
defaultRequest *TokenRequest
|
|
|
|
// store issued tokens
|
|
store tokenIssuerStore
|
|
|
|
// lookup for issued tokens
|
|
lookup tokenIssuerLookup
|
|
|
|
// generator for issued tokens
|
|
generator tokenIssuerGenerator
|
|
|
|
// signer for issued tokens
|
|
signer tokenIssuerSigner
|
|
}
|
|
|
|
// IssuerOptFn modify toeknIssuer
|
|
IssuerOptFn func(*tokenIssuer) error
|
|
|
|
TokenRequest struct {
|
|
AccessToken string
|
|
RefreshToken string
|
|
Expiration time.Duration
|
|
Audience string
|
|
Issuer string
|
|
IssuedAt time.Time
|
|
ClientID uint64
|
|
UserID uint64
|
|
Roles []uint64
|
|
Scope []string
|
|
}
|
|
|
|
// IssueOptFn functions modify TokenRequest
|
|
IssueOptFn func(*TokenRequest) error
|
|
|
|
tokenIssuerStore func(context.Context, TokenRequest) error
|
|
tokenIssuerLookup func(context.Context, string) error
|
|
tokenIssuerGenerator func(context.Context, TokenRequest) (string, string, error)
|
|
tokenIssuerSigner func(token jwt.Token) ([]byte, error)
|
|
)
|
|
|
|
var (
|
|
TokenIssuer *tokenIssuer
|
|
|
|
// wrapper around time.Now() that will aid service testing
|
|
now = func() *time.Time {
|
|
c := time.Now().Truncate(time.Second)
|
|
return &c
|
|
}
|
|
)
|
|
|
|
// NewTokenIssuer initializes and returns new instance of JWT manager
|
|
func NewTokenIssuer(opt ...IssuerOptFn) (issuer *tokenIssuer, err error) {
|
|
issuer = &tokenIssuer{
|
|
defaultRequest: &TokenRequest{Issuer: "cortezaproject.org"},
|
|
|
|
store: func(ctx context.Context, request TokenRequest) error {
|
|
// elegantly handle unconfigured store
|
|
return fmt.Errorf("token issuer store not configured")
|
|
},
|
|
|
|
lookup: func(context.Context, string) error {
|
|
// elegantly handle unconfigured lookup
|
|
return fmt.Errorf("token issuer lookup not configured")
|
|
},
|
|
|
|
signer: func(token jwt.Token) ([]byte, error) {
|
|
// elegantly handle unconfigured signer
|
|
return nil, fmt.Errorf("token issuer signer not configured")
|
|
},
|
|
|
|
generator: DefaultAccessTokenGenerator,
|
|
}
|
|
|
|
return issuer, applyToIssuer(issuer, opt...)
|
|
}
|
|
|
|
// Issue issues new access token, stores it and returns signed JWT.
|
|
func (i *tokenIssuer) Issue(ctx context.Context, opt ...IssueOptFn) (_ []byte, err error) {
|
|
var req = i.newTokenRequest()
|
|
if err = req.apply(opt...); err != nil {
|
|
return
|
|
}
|
|
|
|
if len(req.AccessToken+req.RefreshToken) > 0 {
|
|
panic("can not issue new token with preset access and refresh tokens, " +
|
|
"this is most likely an implementation mistake")
|
|
}
|
|
|
|
if req.AccessToken, req.RefreshToken, err = i.generator(ctx, *req); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = i.store(ctx, *req); err != nil {
|
|
return
|
|
}
|
|
|
|
return i.sign(req)
|
|
}
|
|
|
|
func (i *tokenIssuer) Sign(opt ...IssueOptFn) (_ []byte, err error) {
|
|
var req = i.newTokenRequest()
|
|
if err = req.apply(opt...); err != nil {
|
|
return
|
|
}
|
|
|
|
return i.sign(req)
|
|
}
|
|
|
|
func (i *tokenIssuer) sign(req *TokenRequest) ([]byte, error) {
|
|
if token, err := makeToken(req); err != nil {
|
|
return nil, err
|
|
} else {
|
|
return i.signer(token)
|
|
}
|
|
}
|
|
|
|
// Returns new token request and copies relevant values from the default token request on the issuer
|
|
func (i *tokenIssuer) newTokenRequest() *TokenRequest {
|
|
return &TokenRequest{
|
|
Issuer: i.defaultRequest.Issuer,
|
|
ClientID: i.defaultRequest.ClientID,
|
|
Expiration: i.defaultRequest.Expiration,
|
|
IssuedAt: *now(),
|
|
}
|
|
}
|
|
|
|
// Validate performs token validation by checking existence of access-token in the store
|
|
func (i *tokenIssuer) Validate(ctx context.Context, token jwt.Token) (err error) {
|
|
if err = i.lookup(ctx, token.JwtID()); err != nil {
|
|
return errUnauthorized()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func makeToken(req *TokenRequest) (_ jwt.Token, err error) {
|
|
var (
|
|
roles = make([]string, len(req.Roles))
|
|
token = jwt.New()
|
|
|
|
toString = func(i uint64) string {
|
|
return strconv.FormatUint(i, 10)
|
|
}
|
|
)
|
|
|
|
if len(req.Scope) == 0 {
|
|
// for backward compatibility we default
|
|
// unset scope to profile & api
|
|
req.Scope = []string{"profile", "api"}
|
|
}
|
|
|
|
if req.IssuedAt.IsZero() {
|
|
req.IssuedAt = *now()
|
|
}
|
|
|
|
for i, r := range req.Roles {
|
|
roles[i] = toString(r)
|
|
}
|
|
|
|
// The key part: store access token as JWT ID claim.
|
|
// Claim will be extracted when JWT is validated and checked
|
|
if err = token.Set(jwt.JwtIDKey, req.AccessToken); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = token.Set(jwt.SubjectKey, toString(req.UserID)); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = token.Set(jwt.ExpirationKey, now().Add(req.Expiration).Unix()); err != nil {
|
|
return
|
|
}
|
|
|
|
if req.Audience != "" {
|
|
if err = token.Set(jwt.AudienceKey, req.Audience); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
if err = token.Set(jwt.IssuerKey, req.Issuer); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = token.Set(jwt.IssuedAtKey, req.IssuedAt.Unix()); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = token.Set("clientID", toString(req.ClientID)); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = token.Set("scope", strings.Join(req.Scope, " ")); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = token.Set("roles", roles); err != nil {
|
|
return
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
// IdentityFromToken decodes sub & roles claims into identity
|
|
func IdentityFromToken(token jwt.Token) *identity {
|
|
if token == nil {
|
|
return Anonymous()
|
|
}
|
|
|
|
var (
|
|
roles, _ = token.Get("roles")
|
|
)
|
|
|
|
return Authenticated(
|
|
cast.ToUint64(token.Subject()),
|
|
payload.ParseUint64s(cast.ToStringSlice(roles))...,
|
|
)
|
|
}
|
|
|
|
// DefaultAccessTokenGenerator uses token generator from oauth2 lib
|
|
func DefaultAccessTokenGenerator(ctx context.Context, req TokenRequest) (string, string, error) {
|
|
return generates.NewAccessGenerate().Token(
|
|
ctx,
|
|
&oauth2.GenerateBasic{
|
|
Client: &models.Client{
|
|
ID: strconv.FormatUint(req.ClientID, 10),
|
|
},
|
|
UserID: strconv.FormatUint(req.UserID, 10),
|
|
CreateAt: *now(),
|
|
TokenInfo: nil,
|
|
Request: nil,
|
|
},
|
|
true,
|
|
)
|
|
}
|
|
|
|
// WithSecretSigner configures token issuer with
|
|
func WithSecretSigner(secret string) IssuerOptFn {
|
|
return func(tm *tokenIssuer) (err error) {
|
|
if len(secret) == 0 {
|
|
return fmt.Errorf("JWK missing")
|
|
}
|
|
|
|
var key jwk.Key
|
|
if key, err = jwk.New([]byte(secret)); err != nil {
|
|
return fmt.Errorf("could not parse JWK: %w", err)
|
|
}
|
|
|
|
tm.signer = func(token jwt.Token) ([]byte, error) {
|
|
return jwt.Sign(token, jwa.HS512, key)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithDefaultClientID configures ID of the default auth client
|
|
func WithDefaultClientID(ID uint64) IssuerOptFn {
|
|
return func(tm *tokenIssuer) (err error) {
|
|
tm.defaultRequest.ClientID = ID
|
|
return
|
|
}
|
|
}
|
|
|
|
// WithDefaultExpiration configures default token expiration time
|
|
func WithDefaultExpiration(exp time.Duration) IssuerOptFn {
|
|
return func(tm *tokenIssuer) (err error) {
|
|
tm.defaultRequest.Expiration = exp
|
|
return
|
|
}
|
|
}
|
|
|
|
// WithDefaultIssuer configures default issuer claim
|
|
func WithDefaultIssuer(iss string) IssuerOptFn {
|
|
return func(tm *tokenIssuer) (err error) {
|
|
tm.defaultRequest.Issuer = iss
|
|
return
|
|
}
|
|
}
|
|
|
|
// WithStore configures store function
|
|
func WithStore(fn tokenIssuerStore) IssuerOptFn {
|
|
return func(tm *tokenIssuer) (err error) {
|
|
tm.store = fn
|
|
return
|
|
}
|
|
}
|
|
|
|
// WithLookup configures lookup function
|
|
func WithLookup(fn tokenIssuerLookup) IssuerOptFn {
|
|
return func(tm *tokenIssuer) (err error) {
|
|
tm.lookup = fn
|
|
return
|
|
}
|
|
}
|
|
|
|
// WithGenerator configures generator function
|
|
func WithGenerator(fn tokenIssuerGenerator) IssuerOptFn {
|
|
return func(tm *tokenIssuer) (err error) {
|
|
tm.generator = fn
|
|
return
|
|
}
|
|
}
|
|
|
|
// WithSigner configures signer function
|
|
func WithSigner(fn tokenIssuerSigner) IssuerOptFn {
|
|
return func(tm *tokenIssuer) (err error) {
|
|
tm.signer = fn
|
|
return
|
|
}
|
|
}
|
|
|
|
func (req *TokenRequest) apply(opt ...IssueOptFn) (err error) {
|
|
for _, fn := range opt {
|
|
if err = fn(req); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func WithExpiration(e time.Duration) IssueOptFn {
|
|
return func(t *TokenRequest) (err error) {
|
|
t.Expiration = e
|
|
return
|
|
}
|
|
}
|
|
|
|
func WithIdentity(i Identifiable) IssueOptFn {
|
|
return func(t *TokenRequest) (err error) {
|
|
t.UserID = i.Identity()
|
|
t.Roles = i.Roles()
|
|
return
|
|
}
|
|
}
|
|
|
|
func WithAccessToken(access string) IssueOptFn {
|
|
return func(t *TokenRequest) (err error) {
|
|
t.AccessToken = access
|
|
return
|
|
}
|
|
}
|
|
|
|
func WithScope(ss ...string) IssueOptFn {
|
|
return func(t *TokenRequest) (err error) {
|
|
t.Scope = ss
|
|
return
|
|
}
|
|
}
|
|
|
|
func WithAudience(aud string) IssueOptFn {
|
|
return func(t *TokenRequest) (err error) {
|
|
t.Audience = aud
|
|
return
|
|
}
|
|
}
|
|
|
|
func WithClientID(id uint64) IssueOptFn {
|
|
return func(t *TokenRequest) (err error) {
|
|
t.ClientID = id
|
|
return
|
|
}
|
|
}
|
|
|
|
func applyToIssuer(tm *tokenIssuer, opt ...IssuerOptFn) (err error) {
|
|
for _, fn := range opt {
|
|
if err = fn(tm); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|