Refactor token-issuer (ex-jwt) to be more robust and configurable

This commit is contained in:
Denis Arh
2022-01-21 15:31:10 +01:00
parent 665a15093c
commit 6c3bef0750
40 changed files with 832 additions and 565 deletions
+3 -3
View File
@@ -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
+67 -10
View File
@@ -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)
}
+4 -4
View File
@@ -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")
+2 -23
View File
@@ -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
+13
View File
@@ -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()
}
}
+5 -1
View File
@@ -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))
},
+14 -21
View File
@@ -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 {
+6 -1
View File
@@ -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
//
+47 -27
View File
@@ -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()
}
}
+22 -18
View File
@@ -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,
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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)
+12 -14
View File
@@ -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()
+15 -2
View File
@@ -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()
+1 -1
View File
@@ -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)
+11 -7
View File
@@ -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
}
-271
View File
@@ -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), " "))...,
)
}
-57
View File
@@ -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
//}
+5
View File
@@ -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),
+373
View File
@@ -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
}
}
+44
View File
@@ -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())
})
}
}
+73
View File
@@ -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
}
+4 -7
View File
@@ -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
}
+11 -4
View File
@@ -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,
}
}
+4 -3
View File
@@ -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
+1 -16
View File
@@ -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")
+33 -35
View File
@@ -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")
}
+5 -7
View File
@@ -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
}
+2 -2
View File
@@ -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)
+3 -3
View File
@@ -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
}
+3 -2
View File
@@ -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)
}
+4 -3
View File
@@ -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
}
+3 -2
View File
@@ -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)
}
+13 -1
View File
@@ -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))).
+4
View File
@@ -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))
+2 -2
View File
@@ -11,8 +11,8 @@ import (
func BindAuthMiddleware(r chi.Router) {
r.Use(
auth.JWT().HttpVerifier(),
auth.JWT().HttpValidator("api"),
auth.HttpTokenVerifier,
auth.HttpTokenValidator(),
)
}
+3 -3
View File
@@ -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
}
+11 -7
View File
@@ -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())
}
+3 -2
View File
@@ -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)
}