3
0

Merge branch '2021.9.x-fix-access-token' into 2021.9.x

This commit is contained in:
Denis Arh
2021-09-22 21:36:01 +02:00
19 changed files with 275 additions and 38 deletions
+2
View File
@@ -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)
+10 -5
View File
@@ -30,7 +30,7 @@ type (
actionlog actionlog.Recorder
tokenEncoder auth.TokenEncoder
tokenEncoder auth.TokenGenerator
name string
host string
@@ -290,14 +290,18 @@ func (svc node) Pair(ctx context.Context, nodeID uint64) error {
return err
}
var accessToken string
// Generate JWT token for the federated user
authToken := svc.tokenEncoder.Encode(u)
accessToken, err = svc.tokenEncoder.Generate(ctx, u)
if err != nil {
return err
}
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
n.UpdatedAt = now()
// Start handshake initialization remote node
if err = svc.handshaker.Init(ctx, n, authToken); err != nil {
if err = svc.handshaker.Init(ctx, n, accessToken); err != nil {
return err
}
@@ -357,14 +361,15 @@ func (svc node) HandshakeConfirm(ctx context.Context, nodeID uint64) error {
return err
}
var accessToken string
// Generate JWT token for the federated user
authToken := svc.tokenEncoder.Encode(u)
accessToken, err = svc.tokenEncoder.Generate(ctx, u)
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
n.UpdatedAt = now()
// Complete handshake on remote node
if err = svc.handshaker.Complete(ctx, n, authToken); err != nil {
if err = svc.handshaker.Complete(ctx, n, accessToken); err != nil {
return err
}
+6
View File
@@ -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
+80 -2
View File
@@ -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,33 @@ type (
expiry time.Duration
tokenAuth *jwtauth.JWTAuth
}
tokenStore interface {
LookupUserByID(ctx context.Context, id uint64) (*types.User, error)
LookupAuthOa2tokenByAccess(ctx context.Context, access string) (*types.AuthOa2token, error)
SearchRoleMembers(ctx context.Context, f types.RoleMemberFilter) (types.RoleMemberSet, types.RoleMemberFilter, error)
CreateAuthOa2token(ctx context.Context, rr ...*types.AuthOa2token) error
DeleteAuthOA2TokenByUserID(ctx context.Context, _userID uint64) 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 +65,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 +114,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 +157,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 (
+12 -1
View File
@@ -4,6 +4,7 @@ import (
"net/http"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/go-chi/jwtauth"
)
func MiddlewareValidOnly(next http.Handler) http.Handler {
@@ -15,11 +16,14 @@ func AccessTokenCheck(scope ...string) func(http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx = r.Context()
if !GetIdentityFromContext(ctx).Valid() {
// retrieve token and claims from context
tkn, _, err := jwtauth.FromContext(ctx)
if err != nil || !tkn.Valid {
errors.ProperlyServeHTTP(w, r, ErrUnauthorized(), false)
return
}
// check valid scope
for _, s := range scope {
if !CheckScope(ctx.Value(scopeCtxKey{}), s) {
errors.ProperlyServeHTTP(w, r, ErrUnauthorizedScope(), false)
@@ -27,6 +31,13 @@ func AccessTokenCheck(scope ...string) func(http.Handler) http.Handler {
}
}
// verify JWT from store
_, err = DefaultJwtStore.LookupAuthOa2tokenByAccess(ctx, tkn.Raw)
if err != nil {
errors.ProperlyServeHTTP(w, r, ErrUnauthorized(), false)
return
}
next.ServeHTTP(w, r)
})
}
+18 -3
View File
@@ -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
}
}
+9 -3
View File
@@ -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,
+11
View File
@@ -1458,3 +1458,14 @@ func (svc auth) checkLimits(ctx context.Context) error {
return nil
}
// RemoveAccessTokens removes all user's access tokens when suspended,
// deleted or security context changes
func (svc auth) RemoveAccessTokens(ctx context.Context, user *types.User) error {
return svc.recordAction(
ctx,
&authActionProps{user: user},
AuthActionAccessTokensRemoved,
svc.store.DeleteAuthOA2TokenByUserID(ctx, user.ID),
)
}
+23 -2
View File
@@ -11,12 +11,13 @@ package service
import (
"context"
"fmt"
"strings"
"time"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/locale"
"github.com/cortezaproject/corteza-server/system/types"
"strings"
"time"
)
type (
@@ -636,6 +637,26 @@ func AuthActionEmailOtpVerify(props ...*authActionProps) *authAction {
return a
}
// AuthActionAccessTokensRemoved returns "system:auth.accessTokensRemoved" action
//
// This function is auto-generated.
//
func AuthActionAccessTokensRemoved(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
resource: "system:auth",
action: "accessTokensRemoved",
log: "access tokens for {{user}} removed",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
+2
View File
@@ -83,6 +83,8 @@ actions:
- action: emailOtpVerify
log: "email one-time-password for {{user}} verified"
- action: accessTokensRemoved
log: "access tokens for {{user}} removed"
errors:
- error: invalidCredentials
message: "invalid username and password combination"
+10
View File
@@ -30,6 +30,7 @@ type (
eventbus eventDispatcher
user UserService
auth roleAuth
store store.Storer
@@ -80,6 +81,10 @@ type (
rbacRoleUpdater interface {
UpdateRoles(rr ...*rbac.Role)
}
roleAuth interface {
RemoveAccessTokens(context.Context, *types.User) error
}
)
func Role() *role {
@@ -90,6 +95,7 @@ func Role() *role {
actionlog: DefaultActionlog,
user: DefaultUser,
auth: DefaultAuth,
store: DefaultStore,
system: make(map[string]bool),
@@ -716,6 +722,10 @@ func (svc role) MemberRemove(ctx context.Context, roleID, memberID uint64) (err
return
}
if err = svc.auth.RemoveAccessTokens(ctx, m); err != nil {
return
}
_ = svc.eventbus.WaitFor(ctx, event.RoleMemberAfterRemove(m, r))
return nil
}()
+9
View File
@@ -54,6 +54,7 @@ type (
CheckPasswordStrength(string) bool
SetPasswordCredentials(context.Context, uint64, string) error
RemovePasswordCredentials(context.Context, uint64) error
RemoveAccessTokens(context.Context, *types.User) error
}
userAccessController interface {
@@ -557,6 +558,10 @@ func (svc user) Delete(ctx context.Context, userID uint64) (err error) {
return
}
if err = svc.auth.RemoveAccessTokens(ctx, u); err != nil {
return
}
_ = svc.eventbus.WaitFor(ctx, event.UserAfterDelete(nil, u))
return nil
}()
@@ -646,6 +651,10 @@ func (svc user) Suspend(ctx context.Context, userID uint64) (err error) {
return
}
if err = svc.auth.RemoveAccessTokens(ctx, u); err != nil {
return
}
_ = svc.eventbus.WaitFor(ctx, event.UserAfterSuspend(u, &oldUser))
return nil
}()
+6 -1
View File
@@ -116,10 +116,15 @@ func (h helper) secCtx() context.Context {
func (h helper) apiInit() *apitest.APITest {
InitTestApp()
tkn, err := auth.DefaultJwtHandler.Generate(context.Background(), h.cUser)
if err != nil {
panic(err)
}
return apitest.
New().
Handler(r).
Intercept(helpers.ReqHeaderAuthBearer(h.cUser))
Intercept(helpers.ReqHeaderRawAuthBearer(tkn))
}
+6 -2
View File
@@ -117,11 +117,15 @@ func (h helper) secCtx() context.Context {
func (h helper) apiInit() *apitest.APITest {
InitTestApp()
tkn, err := auth.DefaultJwtHandler.Generate(context.Background(), h.cUser)
if err != nil {
panic(err)
}
return apitest.
New().
Handler(r).
Intercept(helpers.ReqHeaderAuthBearer(h.cUser))
Intercept(helpers.ReqHeaderRawAuthBearer(tkn))
}
func (h helper) mockPermissions(rules ...*rbac.Rule) {
+7 -3
View File
@@ -99,13 +99,17 @@ func (h helper) secCtx() context.Context {
// apitest basics, initialize, set handler, add auth
func (h helper) apiInit() *apitest.APITest {
// InitTestApp()
InitTestApp()
tkn, err := auth.DefaultJwtHandler.Generate(context.Background(), h.cUser)
if err != nil {
panic(err)
}
return apitest.
New().
Handler(r).
Intercept(helpers.ReqHeaderAuthBearer(h.cUser))
Intercept(helpers.ReqHeaderRawAuthBearer(tkn))
}
// Unwraps error before it passes it to the tester
-11
View File
@@ -7,7 +7,6 @@ import (
"github.com/steinfletcher/apitest"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/system/types"
)
func BindAuthMiddleware(r chi.Router) {
@@ -17,16 +16,6 @@ func BindAuthMiddleware(r chi.Router) {
)
}
func ReqHeaderAuthBearer(user *types.User) apitest.Intercept {
return func(req *http.Request) {
if user == nil {
req.Header.Del("Authorization")
} else {
req.Header.Set("Authorization", "Bearer "+auth.DefaultJwtHandler.Encode(user))
}
}
}
func ReqHeaderRawAuthBearer(token string) apitest.Intercept {
return func(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+token)
+6 -3
View File
@@ -139,13 +139,16 @@ func (h helper) secCtx() context.Context {
func (h helper) apiInit() *apitest.APITest {
InitTestApp()
tkn, err := auth.DefaultJwtHandler.Generate(context.Background(), h.cUser)
if err != nil {
panic(err)
}
return apitest.
New().
Handler(r).
Intercept(helpers.ReqHeaderAuthBearer(h.cUser))
Intercept(helpers.ReqHeaderRawAuthBearer(tkn))
}
func (h helper) mockPermissions(rules ...*rbac.Rule) {
h.noError(rbac.Global().Grant(
// TestService we use does not have any backend storage,
+52
View File
@@ -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)
}
+6 -2
View File
@@ -150,11 +150,15 @@ func (h helper) secCtx() context.Context {
func (h helper) apiInit() *apitest.APITest {
InitTestApp()
tkn, err := auth.DefaultJwtHandler.Generate(context.Background(), h.cUser)
if err != nil {
panic(err)
}
return apitest.
New().
Handler(r).
Intercept(helpers.ReqHeaderAuthBearer(h.cUser))
Intercept(helpers.ReqHeaderRawAuthBearer(tkn))
}
func (h helper) mockPermissions(rules ...*rbac.Rule) {