diff --git a/app/app.go b/app/app.go index 499b050d4..ba74b8ccb 100644 --- a/app/app.go +++ b/app/app.go @@ -5,11 +5,11 @@ import ( "net/http" "github.com/cortezaproject/corteza-server/auth/settings" - "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/cortezaproject/corteza-server/pkg/plugin" "github.com/cortezaproject/corteza-server/store" + "github.com/cortezaproject/corteza-server/system/types" "github.com/go-chi/chi/v5" "github.com/go-oauth2/oauth2/v4" "github.com/spf13/cobra" @@ -64,10 +64,10 @@ type ( // CLI Commands Command *cobra.Command - jwt auth.MiddlewareValidator - oa2m oauth2.Manager + DefaultAuthClient *types.AuthClient + // Servers HttpServer httpApiServer GrpcServer grpcServer diff --git a/app/boot_levels.go b/app/boot_levels.go index 91287f9c0..c938395eb 100644 --- a/app/boot_levels.go +++ b/app/boot_levels.go @@ -15,7 +15,6 @@ import ( autService "github.com/cortezaproject/corteza-server/automation/service" cmpService "github.com/cortezaproject/corteza-server/compose/service" cmpEvent "github.com/cortezaproject/corteza-server/compose/service/event" - fdrService "github.com/cortezaproject/corteza-server/federation/service" fedService "github.com/cortezaproject/corteza-server/federation/service" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/apigw" @@ -24,6 +23,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/healthcheck" "github.com/cortezaproject/corteza-server/pkg/http" + "github.com/cortezaproject/corteza-server/pkg/id" "github.com/cortezaproject/corteza-server/pkg/locale" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/mail" @@ -317,27 +317,84 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) { return } + if app.Opt.Auth.DefaultClient != "" { + // default client will help streamline authorization with default clients + app.DefaultAuthClient, err = store.LookupAuthClientByHandle(ctx, app.Store, app.Opt.Auth.DefaultClient) + if err != nil { + return fmt.Errorf("cannot load default client: %w", err) + } + } + { app.oa2m = oauth2.NewManager( app.Opt.Auth, app.Log, - &oauth2.ContextClientStore{}, - &oauth2.CortezaTokenStore{Store: app.Store}, + oauth2.NewClientStore(app.Store, app.DefaultAuthClient), + oauth2.NewTokenStore(app.Store), ) // set base path for links&routes in auth server authHandlers.BasePath = app.Opt.HTTPServer.BaseUrl - if err = auth.SetupDefault(app.oa2m, app.Opt.Auth.Secret, app.Opt.Auth.Expiry); err != nil { - return + auth.DefaultSigner = auth.HmacSigner(app.Opt.Auth.Secret) + + if auth.HttpTokenVerifier, err = auth.TokenVerifierMiddlewareWithSecretSigner(app.Opt.Auth.Secret); err != nil { + return fmt.Errorf("could not set token verifier") } - app.jwt = auth.JWT() + auth.TokenIssuer, err = auth.NewTokenIssuer( + auth.WithSecretSigner(app.Opt.Auth.Secret), + // @todo implement configurable issuer claim + //auth.WithDefaultIssuer(app.Opt.Auth.TokenClaimIssuer), + auth.WithDefaultExpiration(app.Opt.Auth.Expiry), + auth.WithDefaultClientID(app.DefaultAuthClient.ID), + auth.WithLookup(func(ctx context.Context, accessToken string) (err error) { + _, err = store.LookupAuthOa2tokenByAccess(ctx, app.Store, accessToken) + return err + }), + auth.WithStore(func(ctx context.Context, req auth.TokenRequest) error { + var ( + eti = auth.GetExtraReqInfoFromContext(ctx) + createdAt = req.IssuedAt + + oa2t = &types.AuthOa2token{ + ID: id.Next(), + Access: req.AccessToken, + Refresh: req.RefreshToken, + CreatedAt: createdAt, + RemoteAddr: eti.RemoteAddr, + UserAgent: eti.UserAgent, + ClientID: req.ClientID, + UserID: req.UserID, + ExpiresAt: createdAt.Add(req.Expiration), + } + ) + + return store.CreateAuthOa2token(ctx, app.Store, oa2t) + }), + ) + + if err != nil { + return + } } - app.WsServer = websocket.Server(app.Log, app.Opt.Websocket) + app.WsServer = websocket.Server( + app.Log, + app.Opt.Websocket, + func(ctx context.Context, s string) (auth.Identifiable, error) { + //auth.TokenIssuer.Validate(ctx, []byte(s)) + return nil, nil + }, + ) - corredor.Service().SetAuthTokenMaker(app.jwt) + corredor.Service().SetAuthTokenMaker(func(i auth.Identifiable) (signed []byte, err error) { + return auth.TokenIssuer.Issue(ctx, + auth.WithIdentity(i), + auth.WithScope("api", "profile"), + auth.WithAudience("corredor"), + ) + }) ctx = actionlog.RequestOriginToContext(ctx, actionlog.RequestOrigin_APP_Init) defer sentry.Recover() @@ -430,7 +487,7 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) { // // Note: this is a legacy approach, all services from all 3 apps // will most likely be merged in the future - err = fdrService.Initialize(ctx, app.Log, app.Store, fdrService.Config{ + err = fedService.Initialize(ctx, app.Log, app.Store, fedService.Config{ ActionLog: app.Opt.ActionLog, Federation: app.Opt.Federation, }) @@ -510,7 +567,7 @@ func (app *CortezaApp) Activate(ctx context.Context) (err error) { updateSmtpSettings(app.Log, sysService.CurrentSettings) - if app.AuthService, err = authService.New(ctx, app.Log, app.oa2m, app.Store, app.Opt.Auth); err != nil { + if app.AuthService, err = authService.New(ctx, app.Log, app.oa2m, app.Store, app.Opt.Auth, app.DefaultAuthClient); err != nil { return fmt.Errorf("failed to init auth service: %w", err) } diff --git a/app/servers.go b/app/servers.go index 05cd89ce5..58c8749e7 100644 --- a/app/servers.go +++ b/app/servers.go @@ -95,13 +95,13 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) { zap.String("baseUrl", fullpathAPI), ) - r.Route("/system", systemRest.MountRoutes(app.jwt)) - r.Route("/automation", automationRest.MountRoutes(app.jwt)) - r.Route("/compose", composeRest.MountRoutes(app.jwt)) + r.Route("/system", systemRest.MountRoutes()) + r.Route("/automation", automationRest.MountRoutes()) + r.Route("/compose", composeRest.MountRoutes()) r.Route("/websocket", app.WsServer.MountRoutes) if app.Opt.Federation.Enabled { - r.Route("/federation", federationRest.MountRoutes(app.jwt)) + r.Route("/federation", federationRest.MountRoutes()) } var fullpathDocs = options.CleanBase(ho.BaseUrl, ho.ApiBaseUrl, "docs") diff --git a/auth/auth.go b/auth/auth.go index 8da452787..e0aceb88c 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -24,7 +24,6 @@ import ( "github.com/cortezaproject/corteza-server/auth/settings" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/auth" - "github.com/cortezaproject/corteza-server/pkg/handle" "github.com/cortezaproject/corteza-server/pkg/locale" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/cortezaproject/corteza-server/pkg/version" @@ -33,7 +32,6 @@ import ( "github.com/cortezaproject/corteza-server/system/types" "github.com/go-chi/chi/v5" oauth2def "github.com/go-oauth2/oauth2/v4" - "github.com/spf13/cast" "go.uber.org/zap" "golang.org/x/text/language" ) @@ -52,10 +50,9 @@ type ( var PublicAssets embed.FS // New initializes Auth service that orchestrates session manager, oauth2 manager and http request handlers -func New(ctx context.Context, log *zap.Logger, oa2m oauth2def.Manager, s store.Storer, opt options.AuthOpt) (svc *service, err error) { +func New(ctx context.Context, log *zap.Logger, oa2m oauth2def.Manager, s store.Storer, opt options.AuthOpt, defClient *types.AuthClient) (svc *service, err error) { var ( - tpls templateExecutor - defClient *types.AuthClient + tpls templateExecutor ) log = log.Named("auth") @@ -133,14 +130,6 @@ func New(ctx context.Context, log *zap.Logger, oa2m oauth2def.Manager, s store.S return }) - if opt.DefaultClient != "" { - // default client will help streamline authorization with default clients - defClient, err = store.LookupAuthClientByHandle(ctx, s, opt.DefaultClient) - if err != nil { - return nil, fmt.Errorf("cannot load default client: %w", err) - } - } - var ( tplLoader templateLoader @@ -452,16 +441,6 @@ func (svc service) WellKnownOpenIDConfiguration() http.HandlerFunc { } } -func clientLookup(ctx context.Context, s store.AuthClients, identifier interface{}) (*types.AuthClient, error) { - if id := cast.ToUint64(identifier); id > 0 { - return store.LookupAuthClientByID(ctx, s, id) - } else if h := cast.ToString(identifier); handle.IsValid(h) { - return store.LookupAuthClientByHandle(ctx, s, h) - } else { - return nil, systemService.AuthClientErrInvalidID() - } -} - // Profile fills map with user's data // // If scope supports it (contains "profile") user is loaded and diff --git a/auth/client.go b/auth/client.go index 12a420058..4dd1a5ef8 100644 --- a/auth/client.go +++ b/auth/client.go @@ -3,8 +3,11 @@ package auth import ( "context" + "github.com/cortezaproject/corteza-server/pkg/handle" "github.com/cortezaproject/corteza-server/store" + systemService "github.com/cortezaproject/corteza-server/system/service" "github.com/cortezaproject/corteza-server/system/types" + "github.com/spf13/cast" ) type ( @@ -32,3 +35,13 @@ func (svc clientService) Confirmed(ctx context.Context, userID uint64) (types.Au func (svc clientService) Revoke(ctx context.Context, userID, clientID uint64) error { return store.DeleteAuthConfirmedClientByUserIDClientID(ctx, svc.store, userID, clientID) } + +func clientLookup(ctx context.Context, s store.AuthClients, identifier interface{}) (*types.AuthClient, error) { + if id := cast.ToUint64(identifier); id > 0 { + return store.LookupAuthClientByID(ctx, s, id) + } else if h := cast.ToString(identifier); handle.IsValid(h) { + return store.LookupAuthClientByHandle(ctx, s, h) + } else { + return nil, systemService.AuthClientErrInvalidID() + } +} diff --git a/auth/commands/commands.go b/auth/commands/commands.go index 04a38a344..fa9921d61 100644 --- a/auth/commands/commands.go +++ b/auth/commands/commands.go @@ -104,7 +104,11 @@ func Command(ctx context.Context, app serviceInitializer, storeInit func(ctx con err = service.DefaultAuth.LoadRoleMemberships(ctx, user) cli.HandleError(err) - signedToken, err = auth.JWT().Generate(ctx, user, 0, "api", "profile") + signedToken, err = auth.TokenIssuer.Issue(ctx, + auth.WithIdentity(user), + auth.WithScope("profile", "api"), + ) + cli.HandleError(err) cmd.Println(string(signedToken)) }, diff --git a/auth/handlers/handle_oauth2.go b/auth/handlers/handle_oauth2.go index fa7c177fd..25661e12c 100644 --- a/auth/handlers/handle_oauth2.go +++ b/auth/handlers/handle_oauth2.go @@ -18,7 +18,6 @@ import ( oauth2errors "github.com/go-oauth2/oauth2/v4/errors" "github.com/lestrrat-go/jwx/jwt" - "github.com/cortezaproject/corteza-server/auth/oauth2" "github.com/cortezaproject/corteza-server/auth/request" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/errors" @@ -44,7 +43,6 @@ func (h AuthHandlers) oauth2Authorize(req *request.AuthReq) (err error) { request.SetOauth2AuthParams(req.Session, nil) var ( - ctx context.Context client *types.AuthClient ) @@ -52,10 +50,6 @@ func (h AuthHandlers) oauth2Authorize(req *request.AuthReq) (err error) { return err } - // add client to context, now we can reach it from client store via context.Value() fn - // this way we work around the limitations we have with the oauth2 lib. - ctx = context.WithValue(req.Context(), &oauth2.ContextClientStore{}, client) - if client != nil { // No client validation is done at this point; // first, see if user is able to authenticate. @@ -69,9 +63,7 @@ func (h AuthHandlers) oauth2Authorize(req *request.AuthReq) (err error) { // does not send status code! req.Status = -1 - // handle authorize request with extended context that now holds the loaded client! - // we do this - err = h.OAuth2.HandleAuthorizeRequest(req.Response, req.Request.Clone(ctx)) + err = h.OAuth2.HandleAuthorizeRequest(req.Response, req.Request) if err != nil { req.Status = http.StatusInternalServerError req.Template = TmplInternalError @@ -178,9 +170,6 @@ func (h AuthHandlers) oauth2Info(w http.ResponseWriter, r *http.Request) { var ( jt jwt.Token claims map[string]interface{} - - // scope is intentionally left empty - scope = make([]string, 0) ) err := func() (err error) { @@ -188,7 +177,7 @@ func (h AuthHandlers) oauth2Info(w http.ResponseWriter, r *http.Request) { return } - if err = auth.JWT().Validate(r.Context(), jt, scope...); err != nil { + if err = auth.TokenIssuer.Validate(r.Context(), jt); err != nil { return } @@ -277,12 +266,9 @@ func (h AuthHandlers) oauth2authorizeDefaultClientProc(req *request.AuthReq) (er } var ( - // extend context and set default client for oauth2server internals - ctx = context.WithValue(req.Context(), &oauth2.ContextClientStore{}, h.DefaultClient) - // Clone of the initial request // that we'll use for token request validation - r = req.Request.Clone(ctx) + r = req.Request.Clone(req.Context()) ) if _, has := r.Form["code"]; has { @@ -359,9 +345,6 @@ func (h AuthHandlers) handleTokenRequest(req *request.AuthReq, client *types.Aut return h.tokenError(w, fmt.Errorf("invalid client: %w", err)) } - // add client to context: we can reach it from client store via context.Value() fn - // this way we work around the limitations we have with the oauth2 lib. - ctx = context.WithValue(ctx, &oauth2.ContextClientStore{}, client) r = req.Request.Clone(ctx) gt, tgr, err := h.OAuth2.ValidationTokenRequest(r) @@ -411,15 +394,25 @@ func (h AuthHandlers) handleTokenRequest(req *request.AuthReq, client *types.Aut scope = strings.Split(ti.GetScope(), " ") ) - signed, err = auth.JWT().Sign(ti.GetAccess(), user, client.ID, scope...) + signed, err = auth.TokenIssuer.Sign( + auth.WithAccessToken(ti.GetAccess()), + auth.WithIdentity(user), + auth.WithClientID(client.ID), + auth.WithScope(scope...), + ) + if err != nil { return h.tokenError(w, err) } + // modify token info with signed JWT + // this will be sent back to the user ti.SetAccess(string(signed)) response := h.OAuth2.GetTokenData(ti) + // in case client is configured with "openid" scope, + // we'll add "id_token" with all required (by OIDC) details encoded if strings.Contains(client.Scope, "openid") { var idToken []byte if idToken, err = generateIdToken(user, client, ti, h.Opt.BaseURL); err != nil { diff --git a/auth/handlers/mock_test.go b/auth/handlers/mock_test.go index 0ab59b11e..6e20df775 100644 --- a/auth/handlers/mock_test.go +++ b/auth/handlers/mock_test.go @@ -72,7 +72,8 @@ type ( } userServiceMocked struct { - update func(context.Context, *types.User) (*types.User, error) + update func(context.Context, *types.User) (*types.User, error) + findByAny func(context.Context, interface{}) (*types.User, error) } authServiceMocked struct { @@ -105,6 +106,10 @@ func (u userServiceMocked) Update(ctx context.Context, user *types.User) (*types return u.update(ctx, user) } +func (u userServiceMocked) FindByAny(ctx context.Context, any interface{}) (*types.User, error) { + return u.findByAny(ctx, any) +} + // // Mocking authService // diff --git a/auth/oauth2/client.go b/auth/oauth2/client.go index 5bcf5eec6..2e6a57331 100644 --- a/auth/oauth2/client.go +++ b/auth/oauth2/client.go @@ -2,45 +2,65 @@ package oauth2 import ( "context" + "fmt" "strconv" + "github.com/cortezaproject/corteza-server/pkg/handle" + "github.com/cortezaproject/corteza-server/store" + systemService "github.com/cortezaproject/corteza-server/system/service" "github.com/cortezaproject/corteza-server/system/types" "github.com/go-oauth2/oauth2/v4" + "github.com/go-oauth2/oauth2/v4/models" + "github.com/spf13/cast" ) type ( - // Wrapper for store to satisfy oauth2.ClientStore interface - ContextClientStore struct{} + // Wrapper for store to satisfy oauth2.clientStore interface + clientStore struct { + store store.AuthClients + def *types.AuthClient + } ) -var _ oauth2.ClientStore = &ContextClientStore{} +var ( + _ oauth2.ClientStore = &clientStore{} +) -// Pull client directly from context +func NewClientStore(s store.AuthClients, def *types.AuthClient) *clientStore { + return &clientStore{s, def} +} + +// GetByID pulls client directly from context // // This requires that client is put in context before oauth2 procedures are executed! -func (s ContextClientStore) GetByID(ctx context.Context, id string) (oauth2.ClientInfo, error) { - return &clientInfo{ctx.Value(&ContextClientStore{}).(*types.AuthClient)}, nil +func (cs clientStore) GetByID(ctx context.Context, id string) (_ oauth2.ClientInfo, err error) { + var c *types.AuthClient + + if id == "0" || cs.def != nil && cast.ToUint64(id) == cs.def.ID { + if cs.def == nil { + return nil, fmt.Errorf("could not provide default auth client") + } + + c = cs.def + } else if c, err = clientLookup(ctx, cs.store, id); err != nil { + return nil, fmt.Errorf("failed to do auth client lookup (%q): %w", id, err) + } + + m := &models.Client{ + ID: strconv.FormatUint(c.ID, 10), + Secret: c.Secret, + Domain: c.RedirectURI, + } + + return m, nil } -type ( - // Wrapper for client info object to satisfy oauth2.ClientInfo interface - clientInfo struct{ *types.AuthClient } -) - -var _ oauth2.ClientInfo = &clientInfo{} - -func (c clientInfo) GetID() string { - return strconv.FormatUint(c.ID, 10) -} - -func (c clientInfo) GetSecret() string { - return c.Secret -} - -func (c clientInfo) GetDomain() string { - return c.RedirectURI -} - -func (c clientInfo) GetUserID() string { - panic("implement me") +func clientLookup(ctx context.Context, s store.AuthClients, identifier interface{}) (*types.AuthClient, error) { + if id := cast.ToUint64(identifier); id > 0 { + return store.LookupAuthClientByID(ctx, s, id) + } else if h := cast.ToString(identifier); handle.IsValid(h) { + return store.LookupAuthClientByHandle(ctx, s, h) + } else { + return nil, systemService.AuthClientErrInvalidID() + } } diff --git a/auth/oauth2/corteza_token_store.go b/auth/oauth2/corteza_token_store.go index 0a2830817..d2a662219 100644 --- a/auth/oauth2/corteza_token_store.go +++ b/auth/oauth2/corteza_token_store.go @@ -18,11 +18,13 @@ import ( ) type ( - CortezaTokenStore struct { - Store interface { - store.AuthOa2tokens - store.AuthConfirmedClients - } + tokenStorer interface { + store.AuthOa2tokens + store.AuthConfirmedClients + } + + tokenStore struct { + Store tokenStorer } ) @@ -38,18 +40,20 @@ var ( return id.Next() } - _ oauth2.TokenStore = &CortezaTokenStore{} + _ oauth2.TokenStore = &tokenStore{} ) -func (c CortezaTokenStore) Create(ctx context.Context, info oauth2.TokenInfo) (err error) { +func NewTokenStore(s tokenStorer) *tokenStore { + return &tokenStore{Store: s} +} + +func (c tokenStore) Create(ctx context.Context, info oauth2.TokenInfo) (err error) { var ( oa2t *types.AuthOa2token acc *types.AuthConfirmedClient userID uint64 clientID uint64 - - jwtID = id.Next() ) if clientID, err = strconv.ParseUint(info.GetClientID(), 10, 64); err != nil { @@ -61,7 +65,7 @@ func (c CortezaTokenStore) Create(ctx context.Context, info oauth2.TokenInfo) (e } // Make oauth2 token and auth confirmation structs from user and client IDs - if oa2t, acc, err = makeAuthStructs(ctx, jwtID, userID, clientID, info, info.GetCodeExpiresIn()); err != nil { + if oa2t, acc, err = makeAuthStructs(ctx, userID, clientID, info, info.GetCodeExpiresIn()); err != nil { return } @@ -76,19 +80,19 @@ func (c CortezaTokenStore) Create(ctx context.Context, info oauth2.TokenInfo) (e return nil } -func (c CortezaTokenStore) RemoveByCode(ctx context.Context, code string) error { +func (c tokenStore) RemoveByCode(ctx context.Context, code string) error { return store.DeleteAuthOA2TokenByCode(ctx, c.Store, code) } -func (c CortezaTokenStore) RemoveByAccess(ctx context.Context, access string) error { +func (c tokenStore) RemoveByAccess(ctx context.Context, access string) error { return store.DeleteAuthOA2TokenByAccess(ctx, c.Store, access) } -func (c CortezaTokenStore) RemoveByRefresh(ctx context.Context, refresh string) error { +func (c tokenStore) RemoveByRefresh(ctx context.Context, refresh string) error { return store.DeleteAuthOA2TokenByRefresh(ctx, c.Store, refresh) } -func (c CortezaTokenStore) GetByCode(ctx context.Context, code string) (oauth2.TokenInfo, error) { +func (c tokenStore) GetByCode(ctx context.Context, code string) (oauth2.TokenInfo, error) { var ( internal = &oauth2models.Token{} t, err = store.LookupAuthOa2tokenByCode(ctx, c.Store, code) @@ -105,7 +109,7 @@ func (c CortezaTokenStore) GetByCode(ctx context.Context, code string) (oauth2.T return internal, t.Data.Unmarshal(internal) } -func (c CortezaTokenStore) GetByAccess(ctx context.Context, access string) (oauth2.TokenInfo, error) { +func (c tokenStore) GetByAccess(ctx context.Context, access string) (oauth2.TokenInfo, error) { var ( internal = &oauth2models.Token{} t, err = store.LookupAuthOa2tokenByAccess(ctx, c.Store, access) @@ -118,7 +122,7 @@ func (c CortezaTokenStore) GetByAccess(ctx context.Context, access string) (oaut return internal, t.Data.Unmarshal(internal) } -func (c CortezaTokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2.TokenInfo, error) { +func (c tokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2.TokenInfo, error) { var ( internal = &oauth2models.Token{} t, err = store.LookupAuthOa2tokenByRefresh(ctx, c.Store, refresh) @@ -139,14 +143,14 @@ func (c CortezaTokenStore) GetByRefresh(ctx context.Context, refresh string) (oa return internal, t.Data.Unmarshal(internal) } -func makeAuthStructs(ctx context.Context, jwtID, userID, clientID uint64, info oauth2.TokenInfo, expiresAt time.Duration) (oa2t *types.AuthOa2token, acc *types.AuthConfirmedClient, err error) { +func makeAuthStructs(ctx context.Context, userID, clientID uint64, info oauth2.TokenInfo, expiresAt time.Duration) (oa2t *types.AuthOa2token, acc *types.AuthConfirmedClient, err error) { var ( eti = auth.GetExtraReqInfoFromContext(ctx) createdAt = time.Now().Round(time.Second) ) oa2t = &types.AuthOa2token{ - ID: jwtID, + ID: id.Next(), CreatedAt: createdAt, RemoteAddr: eti.RemoteAddr, UserAgent: eti.UserAgent, diff --git a/automation/rest/router.go b/automation/rest/router.go index 289667b2a..ddd6876aa 100644 --- a/automation/rest/router.go +++ b/automation/rest/router.go @@ -7,11 +7,11 @@ import ( "github.com/cortezaproject/corteza-server/pkg/auth" ) -func MountRoutes(mv auth.MiddlewareValidator) func(r chi.Router) { +func MountRoutes() func(r chi.Router) { return func(r chi.Router) { // Protect all _private_ routes r.Group(func(r chi.Router) { - r.Use(mv.HttpValidator("api")) + r.Use(auth.HttpTokenValidator("api")) handlers.NewWorkflow(Workflow{}.New()).MountRoutes(r) handlers.NewTrigger(Trigger{}.New()).MountRoutes(r) diff --git a/compose/rest/router.go b/compose/rest/router.go index 68c11cd90..fb0bce59c 100644 --- a/compose/rest/router.go +++ b/compose/rest/router.go @@ -7,7 +7,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/auth" ) -func MountRoutes(mv auth.MiddlewareValidator) func(r chi.Router) { +func MountRoutes() func(r chi.Router) { return func(r chi.Router) { var ( namespace = Namespace{}.New() @@ -28,7 +28,7 @@ func MountRoutes(mv auth.MiddlewareValidator) func(r chi.Router) { // Protect all _private_ routes r.Group(func(r chi.Router) { - r.Use(mv.HttpValidator("api")) + r.Use(auth.HttpTokenValidator("api")) handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) handlers.NewNamespace(namespace).MountRoutes(r) diff --git a/federation/rest/router.go b/federation/rest/router.go index 9e613e23c..00e55f2dd 100644 --- a/federation/rest/router.go +++ b/federation/rest/router.go @@ -7,7 +7,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/auth" ) -func MountRoutes(mv auth.MiddlewareValidator) func(r chi.Router) { +func MountRoutes() func(r chi.Router) { return func(r chi.Router) { r.Group(func(r chi.Router) { handlers.NewNodeHandshake(NodeHandshake{}.New()).MountRoutes(r) @@ -15,7 +15,7 @@ func MountRoutes(mv auth.MiddlewareValidator) func(r chi.Router) { // Protect all _private_ routes r.Group(func(r chi.Router) { - r.Use(mv.HttpValidator("api")) + r.Use(auth.HttpTokenValidator("api")) handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) diff --git a/federation/service/node.go b/federation/service/node.go index 0d6acdf27..65e449d07 100644 --- a/federation/service/node.go +++ b/federation/service/node.go @@ -24,9 +24,7 @@ const ( ) type ( - tokenGenerator interface { - Generate(ctx context.Context, i auth.Identifiable, clientID uint64, scope ...string) (token []byte, err error) - } + tokenIssuer func(context.Context, auth.Identifiable) (token []byte, err error) node struct { store store.Storer @@ -34,7 +32,7 @@ type ( actionlog actionlog.Recorder - tokenEncoder tokenGenerator + tokenIssuer tokenIssuer name string host string @@ -60,15 +58,15 @@ type ( } ) -func Node(s store.Storer, u service.UserService, al actionlog.Recorder, th tokenGenerator, options options.FederationOpt, ac nodeAccessController) *node { +func Node(s store.Storer, u service.UserService, al actionlog.Recorder, th tokenIssuer, options options.FederationOpt, ac nodeAccessController) *node { return &node{ - store: s, - sysUser: u, - actionlog: al, - tokenEncoder: th, - ac: ac, - name: options.Label, - host: options.Host, + store: s, + sysUser: u, + actionlog: al, + tokenIssuer: th, + ac: ac, + name: options.Label, + host: options.Host, // @todo use HTTP_API_BASE_URL (HTTPServerOpt.ApiBaseUrl) to prefix URI path baseURL: "/federation", @@ -296,7 +294,7 @@ func (svc node) Pair(ctx context.Context, nodeID uint64) error { var accessToken []byte // Generate JWT token for the federated user - accessToken, err = svc.tokenEncoder.Generate(ctx, u, 0) + accessToken, err = svc.tokenIssuer(ctx, u) if err != nil { return err } @@ -367,7 +365,7 @@ func (svc node) HandshakeConfirm(ctx context.Context, nodeID uint64) error { var accessToken []byte // Generate JWT token for the federated user - accessToken, err = svc.tokenEncoder.Generate(ctx, u, 0) + accessToken, err = svc.tokenIssuer(ctx, u) n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity() n.UpdatedAt = now() diff --git a/federation/service/service.go b/federation/service/service.go index 0331a1657..7ee3863f8 100644 --- a/federation/service/service.go +++ b/federation/service/service.go @@ -59,7 +59,7 @@ var ( } ) -func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, c Config) (err error) { +func Initialize(_ context.Context, log *zap.Logger, s store.Storer, c Config) (err error) { DefaultOptions = c.Federation // we're doing conversion to avoid having @@ -86,7 +86,20 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, c Config) DefaultAccessControl = AccessControl() - DefaultNode = Node(DefaultStore, service.DefaultUser, DefaultActionlog, auth.JWT(), c.Federation, DefaultAccessControl) + DefaultNode = Node( + DefaultStore, + service.DefaultUser, + DefaultActionlog, + func(ctx context.Context, i auth.Identifiable) (token []byte, err error) { + return auth.TokenIssuer.Issue( + ctx, + auth.WithIdentity(i), + auth.WithScope("api"), + ) + }, + c.Federation, + DefaultAccessControl, + ) DefaultNodeSync = NodeSync() DefaultExposedModule = ExposedModule() DefaultSharedModule = SharedModule() diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index f89103a15..af42a1b0e 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -86,7 +86,7 @@ func (s server) Serve(ctx context.Context) { } // Verifies JWT in headers, cookies, ... - r.Use(auth.JWT().HttpVerifier()) + r.Use(auth.HttpTokenVerifier) for _, mountRoutes := range s.endpoints { mountRoutes(r) diff --git a/pkg/auth/identity.go b/pkg/auth/identity.go index d28afc0a9..b57405166 100644 --- a/pkg/auth/identity.go +++ b/pkg/auth/identity.go @@ -46,14 +46,18 @@ func (i identity) String() string { func ExtractFromSubClaim(sub string) (userID uint64, rr []uint64) { parts := strings.Split(sub, " ") - rr = make([]uint64, len(parts)-1) - for p := range parts { - id, _ := strconv.ParseUint(parts[p], 10, 64) - if p == 0 { - userID = id - } else { - rr[p-1] = id + + if len(parts) > 1 { + rr = make([]uint64, len(parts)-1) + for p := range parts { + id, _ := strconv.ParseUint(parts[p], 10, 64) + if p == 0 { + userID = id + } else { + rr[p-1] = id + } } } + return } diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go deleted file mode 100644 index d9719e0f7..000000000 --- a/pkg/auth/jwt.go +++ /dev/null @@ -1,271 +0,0 @@ -package auth - -import ( - "context" - "fmt" - "net/http" - "strconv" - "strings" - "time" - - "github.com/cortezaproject/corteza-server/pkg/errors" - "github.com/cortezaproject/corteza-server/pkg/logger" - "github.com/cortezaproject/corteza-server/pkg/payload" - "github.com/go-chi/jwtauth" - "github.com/go-oauth2/oauth2/v4" - "github.com/lestrrat-go/jwx/jwa" - "github.com/lestrrat-go/jwx/jwk" - "github.com/lestrrat-go/jwx/jwt" - "github.com/spf13/cast" - "go.uber.org/zap" -) - -type ( - MiddlewareValidator interface { - HttpValidator(scope ...string) func(http.Handler) http.Handler - Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (signed []byte, err error) - } - - oauth2manager interface { - LoadAccessToken(ctx context.Context, access string) (ti oauth2.TokenInfo, err error) - GenerateAccessToken(ctx context.Context, gt oauth2.GrantType, tgr *oauth2.TokenGenerateRequest) (oauth2.TokenInfo, error) - } - - jwtManager struct { - // Expiration time in minutes - expiry time.Duration - - signAlgo jwa.SignatureAlgorithm - signKey jwk.Key - - log *zap.Logger - - oa2m oauth2manager - - issuerClaim string - } -) - -var ( - defaultJWTManager *jwtManager -) - -// JWT returns d -func JWT() *jwtManager { - return defaultJWTManager -} - -func SetupDefault(oa2m oauth2manager, secret string, expiry time.Duration) (err error) { - // Use JWT secret for hmac signer for now - DefaultSigner = HmacSigner(secret) - defaultJWTManager, err = NewJWTManager(oa2m, jwa.HS512, secret, expiry) - return -} - -// NewJWTManager initializes and returns new instance of JWT manager -// @todo should be extended to accept different kinds of algorythms, private-keys etc. -func NewJWTManager(oa2m oauth2manager, algo jwa.SignatureAlgorithm, secret string, expiry time.Duration) (tm *jwtManager, err error) { - tm = &jwtManager{ - expiry: expiry, - signAlgo: algo, - issuerClaim: "cortezaproject.org", - log: logger.Default(), - oa2m: oa2m, - } - - if len(secret) == 0 { - return nil, fmt.Errorf("JWK missing") - } - - if tm.signKey, err = jwk.New([]byte(secret)); err != nil { - return nil, fmt.Errorf("could not parse JWK: %w", err) - } - - return -} - -// Sign takes security information and returns signed JWT -// -// Access token is expected to be issued by OAuth2 token manager and we want to -// transport access-token one of the JWT claims (JWT ID!). -// -// This way we can perform static checks (origin, validity, expiration) -// before doing any storage lookups. -//f -// Identity holds user ID and all roles that go into this security context -// Client ID represents the auth client that was used -func (m *jwtManager) Sign(accessToken string, identity Identifiable, clientID uint64, scope ...string) (signed []byte, err error) { - var ( - roles string - token = jwt.New() - ) - - if len(scope) == 0 { - // for backward compatibility we default - // unset scope to profile & api - scope = []string{"profile", "api"} - } - - for _, r := range identity.Roles() { - roles += strconv.FormatUint(r, 10) - } - - // this is the key part - // here we put access token to the JWT ID claim - if err = token.Set(jwt.JwtIDKey, accessToken); err != nil { - return - } - - if err = token.Set(jwt.SubjectKey, identity.String()); err != nil { - return - } - - if err = token.Set(jwt.ExpirationKey, time.Now().Add(m.expiry).Unix()); err != nil { - return - } - - if err = token.Set(jwt.IssuerKey, m.issuerClaim); err != nil { - return - } - - if err = token.Set(jwt.IssuedAtKey, time.Now().Unix()); err != nil { - return - } - - if err = token.Set("clientID", strconv.FormatUint(clientID, 10)); err != nil { - return - } - - if err = token.Set("scope", strings.Join(scope, " ")); err != nil { - return - } - - if err = token.Set("roles", strings.TrimSpace(roles)); err != nil { - return - } - - if signed, err = jwt.Sign(token, m.signAlgo, m.signKey); err != nil { - return - } - - return signed, nil -} - -// Generate new access-token and JWT -// -// Why so much effort and not just return the access token? -// We want to transport access-token one of the JWT claims (JWT ID!). -// -// This way we can perform static checks (origin, validity, expiration) -// before doing any storage lookups. -func (m *jwtManager) Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (signed []byte, err error) { - var ( - ti oauth2.TokenInfo - ) - - ti, err = m.oa2m.GenerateAccessToken(ctx, oauth2.Implicit, &oauth2.TokenGenerateRequest{ - ClientID: strconv.FormatUint(clientID, 10), - UserID: i.String(), - Scope: strings.Join(scope, " "), - Refresh: "??????????", - AccessTokenExp: m.expiry, - }) - - if err != nil { - return - } - - return m.Sign(ti.GetAccess(), i, clientID, scope...) -} - -// HttpVerifier http middleware handler will verify a JWT string from a http request. -func (m *jwtManager) HttpVerifier() func(http.Handler) http.Handler { - return jwtauth.Verifier(jwtauth.New(m.signAlgo.String(), m.signKey, nil)) -} - -func (m *jwtManager) HttpValidator(scope ...string) func(http.Handler) http.Handler { - if len(scope) == 0 { - // ensure that scope is not empty - scope = []string{"api"} - } - - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if err := ValidateContext(r.Context(), m.oa2m, scope...); err != nil { - errors.ProperlyServeHTTP(w, r, err, false) - return - } else { - token, _, _ := jwtauth.FromContext(r.Context()) - r = r.WithContext(SetIdentityToContext(r.Context(), IdentityFromToken(token))) - } - - next.ServeHTTP(w, r) - }) - } -} - -func (m *jwtManager) ValidateContext(ctx context.Context, scope ...string) error { - return ValidateContext(ctx, m.oa2m, scope...) -} - -func (m *jwtManager) Validate(ctx context.Context, token jwt.Token, scope ...string) error { - return Validate(ctx, token, m.oa2m, scope...) -} - -// ValidateContext gets JWT & claims from context -// -// It's chi middleware that puts it there -func ValidateContext(ctx context.Context, oa2m oauth2manager, scope ...string) (err error) { - var ( - token jwt.Token - ) - - if token, _, err = jwtauth.FromContext(ctx); err != nil { - return ErrUnauthorized() - } - - return Validate(ctx, token, oa2m, scope...) -} - -// Validate performs token validation -// -// Steps: -// - check scope in the JWT -// - check if JWT ID is set (where the access-token string is stored) -// - check if access-token exists in the DB -// -// -func Validate(ctx context.Context, token jwt.Token, oa2m oauth2manager, scope ...string) (err error) { - if len(scope) > 0 && !CheckJwtScope(token, scope...) { - return ErrUnauthorizedScope() - } - - // Extract the JWT id from the token (string) and convert it to uint64 - // to be compatible with the lookup function - if len(token.JwtID()) < 10 { - return ErrMalformedToken("missing or malformed JWT ID") - } - - // @todo we could use a simple caching mechanism here - // 1. if lookup is successful, add a JWT ID to the list - // 2. add short exp time (that should not last longer than token's exp time) - // 3. check against the list first; if JWT ID is not present there check in storage - // - if _, err = oa2m.LoadAccessToken(ctx, token.JwtID()); err != nil { - return ErrUnauthorized() - } - - return nil -} - -// IdentityFromToken decodes sub & roles claims into identity -func IdentityFromToken(token jwt.Token) *identity { - var ( - roles, _ = token.Get("roles") - ) - - return Authenticated( - cast.ToUint64(token.Subject()), - payload.ParseUint64s(strings.Split(cast.ToString(roles), " "))..., - ) -} diff --git a/pkg/auth/middleware.go b/pkg/auth/middleware.go deleted file mode 100644 index f8e03e00c..000000000 --- a/pkg/auth/middleware.go +++ /dev/null @@ -1,57 +0,0 @@ -package auth - -//import ( -// "net/http" -//) -// -//func MiddlewareValidOnly(next http.Handler) http.Handler { -// return AccessTokenCheck("api")(next) -//} - -//func AccessTokenCheck(s store.AuthOa2tokens, scope ...string) func(http.Handler) http.Handler { -// return func(next http.Handler) http.Handler { -// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -// if err := validateContextToken(r.Context(), s, scope); err != nil { -// errors.ProperlyServeHTTP(w, r, err, false) -// return -// } -// -// next.ServeHTTP(w, r) -// }) -// } -//} -// -//func validateContextToken(ctx context.Context, s store.AuthOa2tokens, scope []string) (err error) { -// var ( -// token jwt.Token -// ) -// -// if token, _, err = jwtauth.FromContext(ctx); err != nil { -// return ErrUnauthorized() -// } -// -// if !CheckJwtScope(token, scope...) { -// return ErrUnauthorizedScope() -// } -// -// // Extract the JWT id from the token (string) and convert it to uint64 -// // to be compatible with the lookup function -// if len(token.JwtID()) < 10 { -// return ErrMalformedToken("missing or malformed JWT ID") -// } -// -// // check if token exists in our DB -// // there is no need to check for anything beyond existence -// // because -// // -// // @todo we could use a simple caching mechanism here -// // 1. if lookup is successful, add a JWT ID to the list -// // 2. add short exp time (that should not last onger than token's exp time) -// // 3. check against the list first; if JWT ID is not present there check in storage -// // -// if _, err = store.LookupAuthOa2tokenByAccess(ctx, s, token.JwtID()); err != nil { -// return ErrUnauthorized() -// } -// -// return nil -//} diff --git a/pkg/auth/signer.go b/pkg/auth/signer.go index 5c722c50a..bd7512ae0 100644 --- a/pkg/auth/signer.go +++ b/pkg/auth/signer.go @@ -21,6 +21,11 @@ var ( DefaultSigner Signer ) +func SetupSigner(secret string) (err error) { + DefaultSigner = HmacSigner(secret) + return +} + func HmacSigner(secret string) *hmacSigner { return &hmacSigner{ secret: []byte(secret), diff --git a/pkg/auth/token_issuer.go b/pkg/auth/token_issuer.go new file mode 100644 index 000000000..734f77ddd --- /dev/null +++ b/pkg/auth/token_issuer.go @@ -0,0 +1,373 @@ +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 + } + + TokenRequest struct { + AccessToken string + RefreshToken string + Expiration time.Duration + Audience string + Issuer string + IssuedAt time.Time + ClientID uint64 + UserID uint64 + Roles []uint64 + Scope []string + } + + IssuerOptFn func(*tokenIssuer) error + 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) (tm *tokenIssuer, err error) { + tm = &tokenIssuer{ + defaultRequest: &TokenRequest{Issuer: "cortezaproject.org"}, + + store: func(ctx context.Context, request TokenRequest) error { + return fmt.Errorf("token issuer store not configured") + }, + + lookup: func(context.Context, string) error { + return fmt.Errorf("token issuer lookup not configured") + }, + + signer: func(token jwt.Token) ([]byte, error) { + return nil, fmt.Errorf("token issuer signer not configured") + }, + + generator: DefaultAccessTokenGenerator, + } + + for _, fn := range opt { + if err = fn(tm); err != nil { + return + } + } + + return +} + +// Issue issues new access token, stores it and returns signed JWT. +func (tm *tokenIssuer) Issue(ctx context.Context, opt ...IssueOptFn) (_ []byte, err error) { + var req = tm.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 = tm.generator(ctx, *req); err != nil { + return + } + + if err = tm.store(ctx, *req); err != nil { + return + } + + return tm.sign(req) +} + +func (tm *tokenIssuer) Sign(opt ...IssueOptFn) (_ []byte, err error) { + var req = tm.newTokenRequest() + if err = req.apply(opt...); err != nil { + return + } + + return tm.sign(req) +} + +func (tm *tokenIssuer) sign(req *TokenRequest) ([]byte, error) { + if token, err := makeToken(req); err != nil { + return nil, err + } else { + return tm.signer(token) + } +} + +// Returns new token request and copies relevant values from the default token request on the issuer +func (tm *tokenIssuer) newTokenRequest() *TokenRequest { + return &TokenRequest{ + Issuer: tm.defaultRequest.Issuer, + ClientID: tm.defaultRequest.ClientID, + Expiration: tm.defaultRequest.Expiration, + IssuedAt: *now(), + } +} + +// Validate performs token validation by checking existence of access-token in the store +func (tm *tokenIssuer) Validate(ctx context.Context, token jwt.Token) (err error) { + if err = tm.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 { + 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 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 + } +} diff --git a/pkg/auth/token_issuer_test.go b/pkg/auth/token_issuer_test.go new file mode 100644 index 000000000..45e9c2e98 --- /dev/null +++ b/pkg/auth/token_issuer_test.go @@ -0,0 +1,44 @@ +package auth + +import ( + "testing" + + "github.com/lestrrat-go/jwx/jwt" + "github.com/stretchr/testify/require" +) + +func TestIdentityDecoding(t *testing.T) { + var ( + req = require.New(t) + ii = []Identifiable{ + &identity{id: 1, memberOf: []uint64{}}, + &identity{id: 2, memberOf: []uint64{2, 3, 4}}, + } + + tm, err = NewTokenIssuer(WithSecretSigner("test")) + ) + + req.NoError(err) + + for _, i := range ii { + t.Run(i.String(), func(t *testing.T) { + var ( + req = require.New(t) + token jwt.Token + signed []byte + ) + + signed, err = tm.Sign(WithIdentity(i)) + req.NoError(err) + + token, err = jwt.Parse(signed) + req.NoError(err) + + ift := IdentityFromToken(token) + + req.Equal(i.Identity(), ift.Identity()) + req.Equal(i.Roles(), ift.Roles()) + }) + + } +} diff --git a/pkg/auth/token_middleware.go b/pkg/auth/token_middleware.go new file mode 100644 index 000000000..e7c842205 --- /dev/null +++ b/pkg/auth/token_middleware.go @@ -0,0 +1,73 @@ +package auth + +import ( + "context" + "fmt" + "net/http" + + "github.com/cortezaproject/corteza-server/pkg/errors" + "github.com/go-chi/jwtauth" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" + "github.com/lestrrat-go/jwx/jwt" +) + +var ( + HttpTokenVerifier func(http.Handler) http.Handler +) + +// TokenVerifierMiddlewareWithSecretSigner returns HTTP handler with simple jwa.HS512 + secret verifier +// +// This should be 1:1 with token issuer! +func TokenVerifierMiddlewareWithSecretSigner(secret string) (_ func(http.Handler) http.Handler, err error) { + if len(secret) == 0 { + return nil, fmt.Errorf("JWK missing") + } + + var key jwk.Key + if key, err = jwk.New([]byte(secret)); err != nil { + return nil, fmt.Errorf("could not parse JWK: %w", err) + } + + return jwtauth.Verifier(jwtauth.New(jwa.HS512.String(), key, nil)), nil +} + +// HttpTokenValidator checks if there is a token with identity and matching scope claim +// +// Empty scope defaults to "api"! +func HttpTokenValidator(scope ...string) func(http.Handler) http.Handler { + if len(scope) == 0 { + // ensure that scope is not empty + scope = []string{"api"} + } + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token, err := verifyToken(r.Context(), TokenIssuer, scope...) + if err != nil { + errors.ProperlyServeHTTP(w, r, err, false) + return + } + + r = r.WithContext(SetIdentityToContext(r.Context(), IdentityFromToken(token))) + next.ServeHTTP(w, r) + }) + } +} + +// pulls token from context and validates scope & access-token +func verifyToken(ctx context.Context, issuer *tokenIssuer, scope ...string) (token jwt.Token, err error) { + if token, _, err = jwtauth.FromContext(ctx); err != nil { + return + } + + if len(scope) > 0 && !CheckJwtScope(token, scope...) { + return nil, ErrUnauthorizedScope() + } + + if err = issuer.Validate(ctx, token); err != nil { + return + } + + return +} diff --git a/pkg/corredor/service.go b/pkg/corredor/service.go index 804f0639d..e489375f4 100644 --- a/pkg/corredor/service.go +++ b/pkg/corredor/service.go @@ -103,9 +103,7 @@ type ( FindByAny(context.Context, interface{}) (*types.Role, error) } - authTokenMaker interface { - Generate(ctx context.Context, i auth.Identifiable, clientID uint64, scope ...string) (signed []byte, err error) - } + authTokenMaker func(i auth.Identifiable) (signed []byte, err error) ) const ( @@ -168,8 +166,7 @@ func NewService(logger *zap.Logger, opt options.CorredorOpt) *service { iteratorProviders: make(map[string]IteratorResourceFinder), - authTokenMaker: auth.JWT(), - eventRegistry: eventbus.Service(), + eventRegistry: eventbus.Service(), denyExec: make(map[string]map[uint64]bool), @@ -735,7 +732,7 @@ func (svc service) exec(ctx context.Context, script string, runAs string, args S } // Generate and save the token - token, err = svc.authTokenMaker.Generate(ctx, definer, 0, "profile", "api") + token, err = svc.authTokenMaker(definer) if err != nil { return } @@ -753,7 +750,7 @@ func (svc service) exec(ctx context.Context, script string, runAs string, args S } // Generate and save the token - token, err = svc.authTokenMaker.Generate(ctx, invoker, 0) + token, err = svc.authTokenMaker(invoker) if err != nil { return } diff --git a/pkg/websocket/server.go b/pkg/websocket/server.go index ec0f128d4..2a4d910bb 100644 --- a/pkg/websocket/server.go +++ b/pkg/websocket/server.go @@ -1,10 +1,12 @@ package websocket import ( + "context" "io" "net/http" "sync" + "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/cortezaproject/corteza-server/pkg/slice" @@ -13,6 +15,8 @@ import ( ) type ( + tokenValidator func(context.Context, string) (auth.Identifiable, error) + server struct { config options.WebsocketOpt logger *zap.Logger @@ -22,6 +26,8 @@ type ( // keep lock on session map changes l sync.RWMutex + + tokenValidator tokenValidator } ) @@ -36,15 +42,16 @@ var ( } ) -func Server(logger *zap.Logger, config options.WebsocketOpt) *server { +func Server(logger *zap.Logger, config options.WebsocketOpt, tv tokenValidator) *server { if !config.LogEnabled { logger = zap.NewNop() } return &server{ - config: config, - logger: logger.Named("websocket"), - sessions: make(map[uint64]map[uint64]io.Writer), + config: config, + logger: logger.Named("websocket"), + sessions: make(map[uint64]map[uint64]io.Writer), + tokenValidator: tv, } } diff --git a/pkg/websocket/server_test.go b/pkg/websocket/server_test.go index 2d2b7aabe..179f970d5 100644 --- a/pkg/websocket/server_test.go +++ b/pkg/websocket/server_test.go @@ -2,16 +2,17 @@ package websocket import ( "bytes" + "testing" + "github.com/cortezaproject/corteza-server/pkg/options" "github.com/stretchr/testify/require" "go.uber.org/zap" - "testing" ) func TestWebsocketSend_NoSessions(t *testing.T) { var ( req = require.New(t) - ws = Server(zap.NewNop(), options.WebsocketOpt{}) + ws = Server(zap.NewNop(), options.WebsocketOpt{}, nil) ) req.NoError(ws.Send("msg", "msg")) @@ -23,7 +24,7 @@ func TestWebsocketSend_NoSessions(t *testing.T) { func TestWebsocketSend_ExistingSessions(t *testing.T) { var ( req = require.New(t) - ws = Server(zap.NewNop(), options.WebsocketOpt{}) + ws = Server(zap.NewNop(), options.WebsocketOpt{}, nil) s1User uint64 = 100 s1ID uint64 = 101 diff --git a/pkg/websocket/session.go b/pkg/websocket/session.go index 2e647e0a5..31a23fb14 100644 --- a/pkg/websocket/session.go +++ b/pkg/websocket/session.go @@ -13,7 +13,6 @@ import ( "github.com/cortezaproject/corteza-server/pkg/id" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/gorilla/websocket" - "github.com/lestrrat-go/jwx/jwt" "go.uber.org/zap" ) @@ -26,10 +25,6 @@ var ( ) type ( - jwtValidator interface { - Validate(ctx context.Context, token jwt.Token, scope ...string) error - } - session struct { id uint64 once sync.Once @@ -49,8 +44,6 @@ type ( identity auth.Identifiable - jv jwtValidator - server *server } ) @@ -62,7 +55,6 @@ func Session(ctx context.Context, ws *server, conn *websocket.Conn) *session { config: ws.config, send: make(chan []byte, 512), stop: make(chan []byte, 1), - jv: auth.JWT(), server: ws, } @@ -294,18 +286,11 @@ func (s *session) writeLoop() error { } func (s *session) authenticate(p *payloadAuth) error { - token, err := jwt.Parse([]byte(p.AccessToken)) + identity, err := s.server.tokenValidator(s.ctx, p.AccessToken) if err != nil { return err } - if err = s.jv.Validate(s.ctx, token, "api"); err != nil { - return err - } - - // Get identity using JWT claims - identity := auth.IdentityFromToken(token) - if s.identity != nil { if s.identity.Identity() != identity.Identity() { return fmt.Errorf("identity does not match") diff --git a/pkg/websocket/session_test.go b/pkg/websocket/session_test.go index 676c8ff96..6aa1b66c1 100644 --- a/pkg/websocket/session_test.go +++ b/pkg/websocket/session_test.go @@ -2,36 +2,47 @@ package websocket import ( "context" + "fmt" "testing" - "time" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/options" - "github.com/lestrrat-go/jwx/jwa" - "github.com/lestrrat-go/jwx/jwt" "github.com/stretchr/testify/require" "go.uber.org/zap" ) -type ( - dummyJwtValidator struct{ err error } -) - -func (d *dummyJwtValidator) Validate(_ context.Context, _ jwt.Token, _ ...string) error { - return d.err -} - func TestSession_procRawMessage(t *testing.T) { var ( req = require.New(t) - s = session{ - server: Server(nil, options.WebsocketOpt{}), - jv: &dummyJwtValidator{}, + + identity1 = auth.Authenticated(123, 456, 789) + identity2 = auth.Authenticated(321, 456, 789) + + s = session{ + server: Server( + nil, + options.WebsocketOpt{}, + func(ctx context.Context, accessToken string) (auth.Identifiable, error) { + //token, err := jwt.Parse([]byte(accessToken)) + //if err != nil { + // return nil, err + //} + //return auth.IdentityFromToken(token), nil + switch accessToken { + case "one": + return identity1, nil + case "two": + return identity2, nil + case "": + return nil, fmt.Errorf("failed to parse token: EOF") + } + + return nil, fmt.Errorf("something else went wrong") + }), } - userID uint64 = 123 - token []byte + token []byte mockResponse = func(token []byte) (out []byte) { out = []byte(`{"@type": "credentials", "@value": {"accessToken": "`) @@ -41,44 +52,31 @@ func TestSession_procRawMessage(t *testing.T) { } ) - jwtManager, err := auth.NewJWTManager(nil, jwa.HS512, "secret", time.Minute) - req.NoError(err) - if testing.Verbose() { s.logger = logger.MakeDebugLogger() } else { s.logger = zap.NewNop() } - req.NoError(err) - - token, err = jwtManager.Sign("access-token", auth.Authenticated(userID, 456, 789), 0, "api") - req.NoError(err) - req.EqualError(s.procRawMessage([]byte("{}")), "unauthenticated session") req.Nil(s.identity) req.EqualError(s.procRawMessage(mockResponse(nil)), "unauthorized: failed to parse token: EOF") req.Nil(s.identity) + token = []byte("one") req.NoError(s.procRawMessage(mockResponse(token))) req.NotNil(s.identity) - req.Equal(userID, s.identity.Identity()) + req.Equal(identity1.Identity(), s.identity.Identity()) req.EqualError(s.procRawMessage([]byte("{}")), "unknown message type ''") - req.Equal(userID, s.identity.Identity()) - - // Repeat with the same user - token, err = jwtManager.Sign("access-token", auth.Authenticated(userID, 456, 789), 0, "api") - req.NoError(err) + req.Equal(identity1.Identity(), s.identity.Identity()) + token = []byte("one") req.NoError(s.procRawMessage(mockResponse(token))) req.NotNil(s.identity) - req.Equal(userID, s.identity.Identity()) - - // Try to authenticate on an existing authenticated session as a different user - token, err = jwtManager.Sign("access-token", auth.Authenticated(userID+1, 456, 789), 0, "api") - req.NoError(err) + req.Equal(identity1.Identity(), s.identity.Identity()) + token = []byte("two") req.EqualError(s.procRawMessage(mockResponse(token)), "unauthorized: identity does not match") } diff --git a/system/rest/auth.go b/system/rest/auth.go index 22d247aaf..9a564c2ac 100644 --- a/system/rest/auth.go +++ b/system/rest/auth.go @@ -19,9 +19,8 @@ type ( } Auth struct { - tokenHandler tokenGenerator - settings *types.AppSettings - authSvc authUserService + settings *types.AppSettings + authSvc authUserService } authUserResponse struct { @@ -51,9 +50,8 @@ type ( func (Auth) New() *Auth { return &Auth{ - tokenHandler: auth.JWT(), - settings: service.CurrentSettings, - authSvc: service.DefaultAuth, + settings: service.CurrentSettings, + authSvc: service.DefaultAuth, } } @@ -75,7 +73,7 @@ func (ctrl *Auth) makePayload(ctx context.Context, user *types.User) (*authUserR } // Generate and save the token - t, err := ctrl.tokenHandler.Generate(ctx, user, 0) + t, err := auth.TokenIssuer.Issue(ctx, auth.WithIdentity(user)) if err != nil { return nil, nil } diff --git a/system/rest/router.go b/system/rest/router.go index 4a2bccf52..3b8f9f4ec 100644 --- a/system/rest/router.go +++ b/system/rest/router.go @@ -8,7 +8,7 @@ import ( "github.com/cortezaproject/corteza-server/system/service" ) -func MountRoutes(mv auth.MiddlewareValidator) func(r chi.Router) { +func MountRoutes() func(r chi.Router) { return func(r chi.Router) { r.Group(func(r chi.Router) { handlers.NewLocale(Locale{}.New()).MountRoutes(r) @@ -26,7 +26,7 @@ func MountRoutes(mv auth.MiddlewareValidator) func(r chi.Router) { // Protect all _private_ routes r.Group(func(r chi.Router) { - r.Use(mv.HttpValidator("api")) + r.Use(auth.HttpTokenValidator("api")) handlers.NewAuthClient(AuthClient{}.New()).MountRoutes(r) handlers.NewAutomation(Automation{}.New()).MountRoutes(r) diff --git a/tests/apigw/main_test.go b/tests/apigw/main_test.go index 9510acacc..c8cefef23 100644 --- a/tests/apigw/main_test.go +++ b/tests/apigw/main_test.go @@ -79,7 +79,7 @@ func InitTestApp() { helpers.BindAuthMiddleware(r) // Sys routes for route management tests - r.Group(rest.MountRoutes(auth.JWT())) + r.Group(rest.MountRoutes()) // API gw routes apigw.Setup(options.Apigw(), service.DefaultLogger, service.DefaultStore) @@ -111,7 +111,8 @@ func newHelper(t *testing.T) helper { helpers.UpdateRBAC(h.roleID) var err error - h.token, err = auth.JWT().Generate(context.Background(), h.cUser, 0) + ctx := context.Background() + h.token, err = auth.TokenIssuer.Issue(ctx, auth.WithIdentity(h.cUser)) if err != nil { panic(err) } @@ -157,7 +158,6 @@ func setup(t *testing.T) (context.Context, helper, store.Storer) { u.SetRoles(auth.BypassRoles().IDs()...) ctx := auth.SetIdentityToContext(context.Background(), u) - return ctx, h, s } diff --git a/tests/automation/main_test.go b/tests/automation/main_test.go index 7374c54ef..9a0311ddf 100644 --- a/tests/automation/main_test.go +++ b/tests/automation/main_test.go @@ -84,7 +84,7 @@ func InitTestApp() { r = chi.NewRouter() r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) - r.Group(rest.MountRoutes(auth.JWT())) + r.Group(rest.MountRoutes()) } } @@ -107,7 +107,8 @@ func newHelper(t *testing.T) helper { helpers.UpdateRBAC(h.roleID) var err error - h.token, err = auth.JWT().Generate(context.Background(), h.cUser, 0) + ctx := context.Background() + h.token, err = auth.TokenIssuer.Issue(ctx, auth.WithIdentity(h.cUser)) if err != nil { panic(err) } diff --git a/tests/compose/main_test.go b/tests/compose/main_test.go index 95427e2b7..03d03d7e1 100644 --- a/tests/compose/main_test.go +++ b/tests/compose/main_test.go @@ -103,7 +103,7 @@ func InitTestApp() { r = chi.NewRouter() r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) - r.Group(rest.MountRoutes(auth.JWT())) + r.Group(rest.MountRoutes()) } } @@ -132,7 +132,9 @@ func newHelper(t *testing.T) helper { func (h *helper) identityToHelper(u *sysTypes.User) { var err error h.cUser = u - h.token, err = auth.JWT().Generate(context.Background(), u, 0) + + ctx := context.Background() + h.token, err = auth.TokenIssuer.Issue(ctx, auth.WithIdentity(h.cUser)) if err != nil { panic(err) } @@ -257,7 +259,6 @@ func setup(t *testing.T) (context.Context, helper, store.Storer) { u.SetRoles(auth.BypassRoles().IDs()...) ctx := auth.SetIdentityToContext(context.Background(), u) - return ctx, h, s } diff --git a/tests/federation/main_test.go b/tests/federation/main_test.go index eb5038330..46efba4df 100644 --- a/tests/federation/main_test.go +++ b/tests/federation/main_test.go @@ -64,7 +64,7 @@ func InitTestApp() { r = chi.NewRouter() r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) - r.Group(rest.MountRoutes(auth.JWT())) + r.Group(rest.MountRoutes()) } } @@ -87,7 +87,8 @@ func newHelper(t *testing.T) helper { helpers.UpdateRBAC(h.roleID) var err error - h.token, err = auth.JWT().Generate(context.Background(), h.cUser, 0) + ctx := context.Background() + h.token, err = auth.TokenIssuer.Issue(ctx, auth.WithIdentity(h.cUser)) if err != nil { panic(err) } diff --git a/tests/federation/node_pairing_test.go b/tests/federation/node_pairing_test.go index e7175a6b4..835b3ba50 100644 --- a/tests/federation/node_pairing_test.go +++ b/tests/federation/node_pairing_test.go @@ -13,6 +13,7 @@ import ( st "github.com/cortezaproject/corteza-server/system/types" "github.com/cortezaproject/corteza-server/tests/helpers" jsonpath "github.com/steinfletcher/apitest-jsonpath" + "github.com/stretchr/testify/require" ) type ( @@ -33,6 +34,7 @@ func (h mockNodeHandshake) Complete(ctx context.Context, n *types.Node, t string func (h helper) clearNodes() { h.noError(store.TruncateFederationNodes(context.Background(), service.DefaultStore)) + h.noError(store.TruncateAuthClients(context.Background(), service.DefaultStore)) } func (h helper) prepareRBAC() { @@ -74,8 +76,18 @@ func TestSuccessfulNodePairing(t *testing.T) { n := h.lookupNodeByID(ID) return n.AuthToken } + + authClient = &st.AuthClient{ + ID: 42, + Handle: "handle", + Secret: "secret", + } + + req = require.New(t) ) + req.NoError(store.CreateAuthClient(context.Background(), service.DefaultStore, authClient)) + service.DefaultNode.SetHandshaker(nil) h.prepareRBAC() @@ -168,7 +180,7 @@ func TestSuccessfulNodePairing(t *testing.T) { service.DefaultNode.SetHandshaker(&mockNodeHandshake{ init: func(ctx context.Context, n *types.Node, authToken string) error { h.apiInit(). - //Debug(). + Debug(). // make sure we do not use test auth-token for authentication but // we do it with pairing token Intercept(helpers.ReqHeaderRawAuthBearer([]byte(n.AuthToken))). diff --git a/tests/helpers/app.go b/tests/helpers/app.go index c221693e7..c829df0a7 100644 --- a/tests/helpers/app.go +++ b/tests/helpers/app.go @@ -9,6 +9,8 @@ import ( "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/cortezaproject/corteza-server/pkg/rand" + "github.com/cortezaproject/corteza-server/system/types" + // Explicitly register SQLite (not done in the app as for testing only) _ "github.com/cortezaproject/corteza-server/store/sqlite3" ) @@ -33,6 +35,8 @@ func NewIntegrationTestApp(ctx context.Context, initTestServices func(*app.Corte a.Log = logger.Default() + a.DefaultAuthClient = &types.AuthClient{ID: 1, Handle: "test-auth-client", Secret: "integration-tests"} + cli.HandleError(a.InitStore(ctx)) cli.HandleError(initTestServices(a)) cli.HandleError(a.InitServices(ctx)) diff --git a/tests/helpers/auth.go b/tests/helpers/auth.go index cf784cf2c..75e73b80e 100644 --- a/tests/helpers/auth.go +++ b/tests/helpers/auth.go @@ -11,8 +11,8 @@ import ( func BindAuthMiddleware(r chi.Router) { r.Use( - auth.JWT().HttpVerifier(), - auth.JWT().HttpValidator("api"), + auth.HttpTokenVerifier, + auth.HttpTokenValidator(), ) } diff --git a/tests/reporter/main_test.go b/tests/reporter/main_test.go index 4be9feaf7..bcabf9d6e 100644 --- a/tests/reporter/main_test.go +++ b/tests/reporter/main_test.go @@ -102,7 +102,7 @@ func InitTestApp() { r = chi.NewRouter() r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) - r.Group(rest.MountRoutes(auth.JWT())) + r.Group(rest.MountRoutes()) } } @@ -125,7 +125,8 @@ func newHelper(t *testing.T) helper { helpers.UpdateRBAC(h.roleID) var err error - h.token, err = auth.JWT().Generate(context.Background(), h.cUser, 0) + ctx := context.Background() + h.token, err = auth.TokenIssuer.Issue(ctx, auth.WithIdentity(h.cUser)) if err != nil { panic(err) } @@ -178,7 +179,6 @@ func setup(t *testing.T) (context.Context, helper, store.Storer) { u.SetRoles(auth.BypassRoles().IDs()...) ctx := auth.SetIdentityToContext(context.Background(), u) - return ctx, h, s } diff --git a/tests/system/auth_test.go b/tests/system/auth_test.go index 91e64aacd..73106eb93 100644 --- a/tests/system/auth_test.go +++ b/tests/system/auth_test.go @@ -2,11 +2,13 @@ package system import ( "context" - "github.com/cortezaproject/corteza-server/system/service" - "github.com/cortezaproject/corteza-server/system/types" "net/http" "testing" + "github.com/cortezaproject/corteza-server/system/service" + "github.com/cortezaproject/corteza-server/system/types" + "github.com/lestrrat-go/jwx/jwt" + "github.com/cortezaproject/corteza-server/tests/helpers" ) @@ -41,12 +43,14 @@ func TestAuthImpersonate(t *testing.T) { JSON(&res) // make sure response has JWT token - jwt := res.Response.JWT - h.a.Greater(len(jwt), 0) + signedToken := res.Response.JWT + h.a.NotEmpty(signedToken) - at, err := service.DefaultStore.LookupAuthOa2tokenByAccess(ctx, jwt) + token, err := jwt.Parse([]byte(signedToken)) + h.a.Nil(err) + + at, err := service.DefaultStore.LookupAuthOa2tokenByAccess(ctx, token.JwtID()) h.a.Nil(err) h.a.NotNil(at) - h.a.Greater(len(at.Access), 0) - h.a.Equal(at.Access, jwt) + h.a.Equal(at.Access, token.JwtID()) } diff --git a/tests/system/main_test.go b/tests/system/main_test.go index e1299c2d7..f1aa82263 100644 --- a/tests/system/main_test.go +++ b/tests/system/main_test.go @@ -116,7 +116,7 @@ func InitTestApp() { r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) - r.Group(rest.MountRoutes(auth.JWT())) + r.Group(rest.MountRoutes()) hh.MountHttpRoutes(r) } } @@ -142,7 +142,8 @@ func newHelper(t *testing.T) helper { h.mockPermissionsWithAccess() var err error - h.token, err = auth.JWT().Generate(context.Background(), h.cUser, 0) + ctx := context.Background() + h.token, err = auth.TokenIssuer.Issue(ctx, auth.WithIdentity(h.cUser)) if err != nil { panic(err) }