Refactor token-issuer (ex-jwt) to be more robust and configurable
This commit is contained in:
+3
-3
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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), " "))...,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
//}
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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))).
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
func BindAuthMiddleware(r chi.Router) {
|
||||
r.Use(
|
||||
auth.JWT().HttpVerifier(),
|
||||
auth.JWT().HttpValidator("api"),
|
||||
auth.HttpTokenVerifier,
|
||||
auth.HttpTokenValidator(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user