From 54cbfe20d8e47755c033be625c58f1dabe62a1f0 Mon Sep 17 00:00:00 2001 From: Vivek Patel Date: Fri, 17 Sep 2021 08:50:26 +0530 Subject: [PATCH] Adds JWT token generation methods - Generates token encoded with user and save it to store - Updated token generation method usage in Corredor and Impersonalization --- app/boot_levels.go | 2 ++ pkg/auth/interfaces.go | 6 ++++ pkg/auth/jwt.go | 76 +++++++++++++++++++++++++++++++++++++-- pkg/corredor/service.go | 21 +++++++++-- system/rest/auth.go | 12 +++++-- tests/system/auth_test.go | 52 +++++++++++++++++++++++++++ 6 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 tests/system/auth_test.go diff --git a/app/boot_levels.go b/app/boot_levels.go index 163be24f0..253532efd 100644 --- a/app/boot_levels.go +++ b/app/boot_levels.go @@ -406,6 +406,8 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) { return } + auth.SetJWTStore(app.Store) + corredor.Service().SetUserFinder(sysService.DefaultUser) corredor.Service().SetRoleFinder(sysService.DefaultRole) diff --git a/pkg/auth/interfaces.go b/pkg/auth/interfaces.go index c46300ae6..d4f3d0001 100644 --- a/pkg/auth/interfaces.go +++ b/pkg/auth/interfaces.go @@ -1,6 +1,7 @@ package auth import ( + "context" "github.com/dgrijalva/jwt-go" "net/http" ) @@ -17,8 +18,13 @@ type ( Encode(identity Identifiable, scope ...string) string } + TokenGenerator interface { + Generate(ctx context.Context, identity Identifiable) (string, error) + } + TokenHandler interface { TokenEncoder + TokenGenerator Authenticate(token string) (jwt.MapClaims, error) HttpVerifier() func(http.Handler) http.Handler HttpAuthenticator() func(http.Handler) http.Handler diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index 0e3dc93d0..7812566e3 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -2,7 +2,10 @@ package auth import ( "context" + "encoding/json" "fmt" + "github.com/cortezaproject/corteza-server/pkg/id" + "github.com/cortezaproject/corteza-server/system/types" "net/http" "strconv" "strings" @@ -20,17 +23,27 @@ type ( expiry time.Duration tokenAuth *jwtauth.JWTAuth } + + tokenStore interface { + CreateAuthOa2token(ctx context.Context, rr ...*types.AuthOa2token) error + UpsertAuthConfirmedClient(ctx context.Context, rr ...*types.AuthConfirmedClient) error + } + + ExtraReqInfo struct { + RemoteAddr string + UserAgent string + } ) var ( DefaultJwtHandler TokenHandler + DefaultJwtStore tokenStore ) func SetupDefault(secret string, expiry time.Duration) { // Use JWT secret for hmac signer for now DefaultSigner = HmacSigner(secret) DefaultJwtHandler, _ = JWT(secret, expiry) - } func JWT(secret string, expiry time.Duration) (tkn *token, err error) { @@ -46,6 +59,13 @@ func JWT(secret string, expiry time.Duration) (tkn *token, err error) { return tkn, nil } +// SetJWTStore set store for JWT +// @todo find better way to initiate store, +// it mainly used for generating and storing accessToken for impersonate and corredor, Ref: j.Generate() +func SetJWTStore(store tokenStore) { + DefaultJwtStore = store +} + func (t *token) Authenticate(token string) (jwt.MapClaims, error) { dt, err := t.tokenAuth.Decode(token) if err != nil { @@ -88,7 +108,6 @@ func (t *token) Encode(i Identifiable, scope ...string) string { } func (t *token) encode(i Identifiable, clientID uint64, scope ...string) string { - roles := "" for _, r := range i.Roles() { roles += fmt.Sprintf(" %d", r) @@ -132,6 +151,59 @@ func (t *token) HttpAuthenticator() func(http.Handler) http.Handler { } } +func (t *token) Generate(ctx context.Context, i Identifiable) (tokenString string, err error) { + var ( + eti = GetExtraReqInfoFromContext(ctx) + oa2t = &types.AuthOa2token{ + ID: id.Next(), + CreatedAt: time.Now().Round(time.Second), + RemoteAddr: eti.RemoteAddr, + UserAgent: eti.UserAgent, + } + + acc = &types.AuthConfirmedClient{ + ConfirmedAt: oa2t.CreatedAt, + } + ) + + tokenString = t.Encode(i) + oa2t.Access = tokenString + oa2t.ExpiresAt = oa2t.CreatedAt.Add(t.expiry) + + if oa2t.Data, err = json.Marshal(oa2t); err != nil { + return + } + + // extend this with the client + oa2t.ClientID = 0 + + // copy client id to auth client confirmation + acc.ClientID = oa2t.ClientID + + if oa2t.UserID, _ = ExtractFromSubClaim(i.String()); oa2t.UserID == 0 { + // UserID stores collection of IDs: user's ID and set of all roles' user is member of + return "", fmt.Errorf("could not parse user ID from token") + } + + // copy user id to auth client confirmation + acc.UserID = oa2t.UserID + + if err = DefaultJwtStore.UpsertAuthConfirmedClient(ctx, acc); err != nil { + return + } + + return tokenString, DefaultJwtStore.CreateAuthOa2token(ctx, oa2t) +} + +func GetExtraReqInfoFromContext(ctx context.Context) ExtraReqInfo { + eti := ctx.Value(ExtraReqInfo{}) + if eti != nil { + return eti.(ExtraReqInfo) + } else { + return ExtraReqInfo{} + } +} + // ClaimsToIdentity decodes sub & roles claims into identity func ClaimsToIdentity(c jwt.MapClaims) (i *identity) { var ( diff --git a/pkg/corredor/service.go b/pkg/corredor/service.go index ef0d3f36c..89f73f154 100644 --- a/pkg/corredor/service.go +++ b/pkg/corredor/service.go @@ -104,7 +104,8 @@ type ( } authTokenMaker interface { - Encode(auth.Identifiable, ...string) string + auth.TokenEncoder + auth.TokenGenerator } ) @@ -653,6 +654,8 @@ func (svc service) exec(ctx context.Context, script string, runAs string, args S zap.String("eventType", args.EventType()), zap.String("resourceType", args.ResourceType()), ) + + token string ) log.Debug("triggered") @@ -716,7 +719,13 @@ func (svc service) exec(ctx context.Context, script string, runAs string, args S return } - if err = encodeArguments(req.Args, "authToken", svc.authTokenMaker.Encode(definer)); err != nil { + // Generate and save the token + token, err = svc.authTokenMaker.Generate(ctx, definer) + if err != nil { + return + } + + if err = encodeArguments(req.Args, "authToken", token); err != nil { return } @@ -728,7 +737,13 @@ func (svc service) exec(ctx context.Context, script string, runAs string, args S return } - if err = encodeArguments(req.Args, "authToken", svc.authTokenMaker.Encode(invoker)); err != nil { + // Generate and save the token + token, err = svc.authTokenMaker.Generate(ctx, invoker) + if err != nil { + return + } + + if err = encodeArguments(req.Args, "authToken", token); err != nil { return } } diff --git a/system/rest/auth.go b/system/rest/auth.go index 61390147d..7c58e8418 100644 --- a/system/rest/auth.go +++ b/system/rest/auth.go @@ -14,7 +14,7 @@ var _ = errors.Wrap type ( Auth struct { - tokenEncoder auth.TokenEncoder + tokenHandler auth.TokenGenerator settings *types.AppSettings authSvc authUserService } @@ -46,7 +46,7 @@ type ( func (Auth) New() *Auth { return &Auth{ - tokenEncoder: auth.DefaultJwtHandler, + tokenHandler: auth.DefaultJwtHandler, settings: service.CurrentSettings, authSvc: service.DefaultAuth, } @@ -69,8 +69,14 @@ func (ctrl *Auth) makePayload(ctx context.Context, user *types.User) (*authUserR return nil, err } + // Generate and save the token + t, err := ctrl.tokenHandler.Generate(ctx, user) + if err != nil { + return nil, nil + } + return &authUserResponse{ - JWT: ctrl.tokenEncoder.Encode(user), + JWT: t, User: &authUserPayload{ userPayload: &userPayload{ ID: user.ID, diff --git a/tests/system/auth_test.go b/tests/system/auth_test.go new file mode 100644 index 000000000..91e64aacd --- /dev/null +++ b/tests/system/auth_test.go @@ -0,0 +1,52 @@ +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/tests/helpers" +) + +func TestAuthImpersonate(t *testing.T) { + + var ( + h = newHelper(t) + ctx = context.Background() + user = h.createUserWithEmail(h.randEmail()) + input = &struct { + UserID uint64 `json:",string"` + }{ + UserID: user.ID, + } + ) + + helpers.AllowMe(h, types.UserRbacResource(user.ID), "impersonate") + + var res struct { + Response struct { + JWT string `json:"jwt"` + } `json:"response"` + } + h.apiInit(). + Post("/auth/impersonate"). + Header("Accept", "application/json"). + JSON(helpers.JSON(input)). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End(). + JSON(&res) + + // make sure response has JWT token + jwt := res.Response.JWT + h.a.Greater(len(jwt), 0) + + at, err := service.DefaultStore.LookupAuthOa2tokenByAccess(ctx, jwt) + h.a.Nil(err) + h.a.NotNil(at) + h.a.Greater(len(at.Access), 0) + h.a.Equal(at.Access, jwt) +}