From 1f3dec6ea628bcf12e64ad1dbf0b34a610c0be68 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 24 Mar 2022 13:31:56 +0530 Subject: [PATCH 01/18] feat: add validate_jwt_token query Resolves #149 --- server/graph/generated/generated.go | 227 +++++++++++++++++++++++++ server/graph/model/models_gen.go | 10 ++ server/graph/schema.graphqls | 11 ++ server/graph/schema.resolvers.go | 10 +- server/resolvers/validate_jwt_token.go | 86 ++++++++++ server/test/resolvers_test.go | 1 + server/test/validate_jwt_token_test.go | 90 ++++++++++ server/token/auth_token.go | 14 +- server/token/jwt.go | 56 ++++++ server/utils/common.go | 22 +++ 10 files changed, 523 insertions(+), 4 deletions(-) create mode 100644 server/resolvers/validate_jwt_token.go create mode 100644 server/test/validate_jwt_token_test.go diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 3dd7e42..9d5300e 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -144,6 +144,7 @@ type ComplexityRoot struct { Profile func(childComplexity int) int Session func(childComplexity int, params *model.SessionQueryInput) int Users func(childComplexity int, params *model.PaginatedInput) int + ValidateJwtToken func(childComplexity int, params model.ValidateJWTTokenInput) int VerificationRequests func(childComplexity int, params *model.PaginatedInput) int } @@ -176,6 +177,10 @@ type ComplexityRoot struct { Users func(childComplexity int) int } + ValidateJWTTokenResponse struct { + IsValid func(childComplexity int) int + } + VerificationRequest struct { CreatedAt func(childComplexity int) int Email func(childComplexity int) int @@ -217,6 +222,7 @@ type QueryResolver interface { Meta(ctx context.Context) (*model.Meta, error) Session(ctx context.Context, params *model.SessionQueryInput) (*model.AuthResponse, error) Profile(ctx context.Context) (*model.User, error) + ValidateJwtToken(ctx context.Context, params model.ValidateJWTTokenInput) (*model.ValidateJWTTokenResponse, error) Users(ctx context.Context, params *model.PaginatedInput) (*model.Users, error) VerificationRequests(ctx context.Context, params *model.PaginatedInput) (*model.VerificationRequests, error) AdminSession(ctx context.Context) (*model.Response, error) @@ -897,6 +903,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.Users(childComplexity, args["params"].(*model.PaginatedInput)), true + case "Query.validate_jwt_token": + if e.complexity.Query.ValidateJwtToken == nil { + break + } + + args, err := ec.field_Query_validate_jwt_token_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.ValidateJwtToken(childComplexity, args["params"].(model.ValidateJWTTokenInput)), true + case "Query._verification_requests": if e.complexity.Query.VerificationRequests == nil { break @@ -1049,6 +1067,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Users.Users(childComplexity), true + case "ValidateJWTTokenResponse.is_valid": + if e.complexity.ValidateJWTTokenResponse.IsValid == nil { + break + } + + return e.complexity.ValidateJWTTokenResponse.IsValid(childComplexity), true + case "VerificationRequest.created_at": if e.complexity.VerificationRequest.CreatedAt == nil { break @@ -1318,6 +1343,10 @@ type Env { ORGANIZATION_LOGO: String } +type ValidateJWTTokenResponse { + is_valid: Boolean! +} + input UpdateEnvInput { ADMIN_SECRET: String CUSTOM_ACCESS_TOKEN_SCRIPT: String @@ -1473,6 +1502,12 @@ input InviteMemberInput { redirect_uri: String } +input ValidateJWTTokenInput { + token_type: String! + token: String! + roles: [String!] +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -1498,6 +1533,7 @@ type Query { meta: Meta! session(params: SessionQueryInput): AuthResponse! profile: User! + validate_jwt_token(params: ValidateJWTTokenInput!): ValidateJWTTokenResponse! # admin only apis _users(params: PaginatedInput): Users! _verification_requests(params: PaginatedInput): VerificationRequests! @@ -1797,6 +1833,21 @@ func (ec *executionContext) field_Query_session_args(ctx context.Context, rawArg return args, nil } +func (ec *executionContext) field_Query_validate_jwt_token_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.ValidateJWTTokenInput + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNValidateJWTTokenInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐValidateJWTTokenInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + return args, nil +} + func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4598,6 +4649,48 @@ func (ec *executionContext) _Query_profile(ctx context.Context, field graphql.Co return ec.marshalNUser2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUser(ctx, field.Selections, res) } +func (ec *executionContext) _Query_validate_jwt_token(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Query_validate_jwt_token_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().ValidateJwtToken(rctx, args["params"].(model.ValidateJWTTokenInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.ValidateJWTTokenResponse) + fc.Result = res + return ec.marshalNValidateJWTTokenResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐValidateJWTTokenResponse(ctx, field.Selections, res) +} + func (ec *executionContext) _Query__users(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5487,6 +5580,41 @@ func (ec *executionContext) _Users_users(ctx context.Context, field graphql.Coll return ec.marshalNUser2ᚕᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUserᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _ValidateJWTTokenResponse_is_valid(ctx context.Context, field graphql.CollectedField, obj *model.ValidateJWTTokenResponse) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ValidateJWTTokenResponse", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsValid, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _VerificationRequest_id(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequest) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -8025,6 +8153,45 @@ func (ec *executionContext) unmarshalInputUpdateUserInput(ctx context.Context, o return it, nil } +func (ec *executionContext) unmarshalInputValidateJWTTokenInput(ctx context.Context, obj interface{}) (model.ValidateJWTTokenInput, error) { + var it model.ValidateJWTTokenInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "token_type": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("token_type")) + it.TokenType, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "token": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("token")) + it.Token, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "roles": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("roles")) + it.Roles, err = ec.unmarshalOString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputVerifyEmailInput(ctx context.Context, obj interface{}) (model.VerifyEmailInput, error) { var it model.VerifyEmailInput asMap := map[string]interface{}{} @@ -8515,6 +8682,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } return res }) + case "validate_jwt_token": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_validate_jwt_token(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) case "_users": field := field out.Concurrently(i, func() (res graphql.Marshaler) { @@ -8716,6 +8897,33 @@ func (ec *executionContext) _Users(ctx context.Context, sel ast.SelectionSet, ob return out } +var validateJWTTokenResponseImplementors = []string{"ValidateJWTTokenResponse"} + +func (ec *executionContext) _ValidateJWTTokenResponse(ctx context.Context, sel ast.SelectionSet, obj *model.ValidateJWTTokenResponse) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, validateJWTTokenResponseImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ValidateJWTTokenResponse") + case "is_valid": + out.Values[i] = ec._ValidateJWTTokenResponse_is_valid(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var verificationRequestImplementors = []string{"VerificationRequest"} func (ec *executionContext) _VerificationRequest(ctx context.Context, sel ast.SelectionSet, obj *model.VerificationRequest) graphql.Marshaler { @@ -9345,6 +9553,25 @@ func (ec *executionContext) marshalNUsers2ᚖgithubᚗcomᚋauthorizerdevᚋauth return ec._Users(ctx, sel, v) } +func (ec *executionContext) unmarshalNValidateJWTTokenInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐValidateJWTTokenInput(ctx context.Context, v interface{}) (model.ValidateJWTTokenInput, error) { + res, err := ec.unmarshalInputValidateJWTTokenInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNValidateJWTTokenResponse2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐValidateJWTTokenResponse(ctx context.Context, sel ast.SelectionSet, v model.ValidateJWTTokenResponse) graphql.Marshaler { + return ec._ValidateJWTTokenResponse(ctx, sel, &v) +} + +func (ec *executionContext) marshalNValidateJWTTokenResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐValidateJWTTokenResponse(ctx context.Context, sel ast.SelectionSet, v *model.ValidateJWTTokenResponse) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._ValidateJWTTokenResponse(ctx, sel, v) +} + func (ec *executionContext) marshalNVerificationRequest2ᚕᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐVerificationRequestᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.VerificationRequest) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 1ebad63..ec260cf 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -256,6 +256,16 @@ type Users struct { Users []*User `json:"users"` } +type ValidateJWTTokenInput struct { + TokenType string `json:"token_type"` + Token string `json:"token"` + Roles []string `json:"roles"` +} + +type ValidateJWTTokenResponse struct { + IsValid bool `json:"is_valid"` +} + type VerificationRequest struct { ID string `json:"id"` Identifier *string `json:"identifier"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 13f2a1b..65cdf67 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -126,6 +126,10 @@ type Env { ORGANIZATION_LOGO: String } +type ValidateJWTTokenResponse { + is_valid: Boolean! +} + input UpdateEnvInput { ADMIN_SECRET: String CUSTOM_ACCESS_TOKEN_SCRIPT: String @@ -281,6 +285,12 @@ input InviteMemberInput { redirect_uri: String } +input ValidateJWTTokenInput { + token_type: String! + token: String! + roles: [String!] +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -306,6 +316,7 @@ type Query { meta: Meta! session(params: SessionQueryInput): AuthResponse! profile: User! + validate_jwt_token(params: ValidateJWTTokenInput!): ValidateJWTTokenResponse! # admin only apis _users(params: PaginatedInput): Users! _verification_requests(params: PaginatedInput): VerificationRequests! diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index e4f9275..5b8501c 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -91,6 +91,10 @@ func (r *queryResolver) Profile(ctx context.Context) (*model.User, error) { return resolvers.ProfileResolver(ctx) } +func (r *queryResolver) ValidateJwtToken(ctx context.Context, params model.ValidateJWTTokenInput) (*model.ValidateJWTTokenResponse, error) { + return resolvers.ValidateJwtTokenResolver(ctx, params) +} + func (r *queryResolver) Users(ctx context.Context, params *model.PaginatedInput) (*model.Users, error) { return resolvers.UsersResolver(ctx, params) } @@ -113,5 +117,7 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } -type mutationResolver struct{ *Resolver } -type queryResolver struct{ *Resolver } +type ( + mutationResolver struct{ *Resolver } + queryResolver struct{ *Resolver } +) diff --git a/server/resolvers/validate_jwt_token.go b/server/resolvers/validate_jwt_token.go new file mode 100644 index 0000000..ce1c84c --- /dev/null +++ b/server/resolvers/validate_jwt_token.go @@ -0,0 +1,86 @@ +package resolvers + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/sessionstore" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" + "github.com/golang-jwt/jwt" +) + +// ValidateJwtTokenResolver is used to validate a jwt token without its rotation +// this can be used at API level (backend) +// it can validate: +// access_token +// id_token +// refresh_token +func ValidateJwtTokenResolver(ctx context.Context, params model.ValidateJWTTokenInput) (*model.ValidateJWTTokenResponse, error) { + gc, err := utils.GinContextFromContext(ctx) + if err != nil { + return nil, err + } + + tokenType := params.TokenType + if tokenType != "access_token" && tokenType != "refresh_token" && tokenType != "id_token" { + return nil, errors.New("invalid token type") + } + + userID := "" + nonce := "" + // access_token and refresh_token should be validated from session store as well + if tokenType == "access_token" || tokenType == "refresh_token" { + savedSession := sessionstore.GetState(params.Token) + if savedSession == "" { + return &model.ValidateJWTTokenResponse{ + IsValid: false, + }, nil + } + savedSessionSplit := strings.Split(savedSession, "@") + nonce = savedSessionSplit[0] + userID = savedSessionSplit[1] + } + + hostname := utils.GetHost(gc) + var claimRoles []string + var claims jwt.MapClaims + + // we cannot validate sub and nonce in case of id_token as that token is not persisted in session store + if userID != "" && nonce != "" { + claims, err = token.ParseJWTToken(params.Token, hostname, nonce, userID) + if err != nil { + return &model.ValidateJWTTokenResponse{ + IsValid: false, + }, nil + } + } else { + claims, err = token.ParseJWTTokenWithoutNonce(params.Token, hostname) + if err != nil { + return &model.ValidateJWTTokenResponse{ + IsValid: false, + }, nil + } + + } + + claimRolesInterface := claims["roles"] + roleSlice := utils.ConvertInterfaceToSlice(claimRolesInterface) + for _, v := range roleSlice { + claimRoles = append(claimRoles, v.(string)) + } + + if params.Roles != nil && len(params.Roles) > 0 { + for _, v := range params.Roles { + if !utils.StringSliceContains(claimRoles, v) { + return nil, fmt.Errorf(`unauthorized`) + } + } + } + return &model.ValidateJWTTokenResponse{ + IsValid: true, + }, nil +} diff --git a/server/test/resolvers_test.go b/server/test/resolvers_test.go index 7e0c41d..b64695d 100644 --- a/server/test/resolvers_test.go +++ b/server/test/resolvers_test.go @@ -63,6 +63,7 @@ func TestResolvers(t *testing.T) { logoutTests(t, s) metaTests(t, s) inviteUserTest(t, s) + validateJwtTokenTest(t, s) }) } } diff --git a/server/test/validate_jwt_token_test.go b/server/test/validate_jwt_token_test.go new file mode 100644 index 0000000..5bb4268 --- /dev/null +++ b/server/test/validate_jwt_token_test.go @@ -0,0 +1,90 @@ +package test + +import ( + "testing" + "time" + + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/resolvers" + "github.com/authorizerdev/authorizer/server/sessionstore" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func validateJwtTokenTest(t *testing.T, s TestSetup) { + t.Helper() + _, ctx := createContext(s) + t.Run(`validate params`, func(t *testing.T) { + res, err := resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{ + TokenType: "access_token", + Token: "", + }) + assert.False(t, res.IsValid) + res, err = resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{ + TokenType: "access_token", + Token: "invalid", + }) + assert.False(t, res.IsValid) + _, err = resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{ + TokenType: "access_token_invalid", + Token: "invalid@invalid", + }) + assert.Error(t, err, "invalid token") + }) + + scope := []string{"openid", "email", "profile", "offline_access"} + user := models.User{ + ID: uuid.New().String(), + Email: "jwt_test_" + s.TestInfo.Email, + Roles: "user", + UpdatedAt: time.Now().Unix(), + CreatedAt: time.Now().Unix(), + } + + roles := []string{"user"} + gc, err := utils.GinContextFromContext(ctx) + assert.NoError(t, err) + authToken, err := token.CreateAuthToken(gc, user, roles, scope) + sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) + sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID) + + t.Run(`should validate the access token`, func(t *testing.T) { + res, err := resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{ + TokenType: "access_token", + Token: authToken.AccessToken.Token, + Roles: []string{"user"}, + }) + + assert.NoError(t, err) + assert.True(t, res.IsValid) + + res, err = resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{ + TokenType: "access_token", + Token: authToken.AccessToken.Token, + Roles: []string{"invalid_role"}, + }) + + assert.Error(t, err) + }) + + t.Run(`should validate the refresh token`, func(t *testing.T) { + res, err := resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{ + TokenType: "refresh_token", + Token: authToken.RefreshToken.Token, + }) + assert.NoError(t, err) + assert.True(t, res.IsValid) + }) + + t.Run(`should validate the id token`, func(t *testing.T) { + res, err := resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{ + TokenType: "id_token", + Token: authToken.IDToken.Token, + }) + assert.NoError(t, err) + assert.True(t, res.IsValid) + }) +} diff --git a/server/token/auth_token.go b/server/token/auth_token.go index 350da17..8714792 100644 --- a/server/token/auth_token.go +++ b/server/token/auth_token.go @@ -161,7 +161,12 @@ func GetAccessToken(gc *gin.Context) (string, error) { return "", fmt.Errorf(`unauthorized`) } - if !strings.HasPrefix(auth, "Bearer ") { + authSplit := strings.Split(auth, " ") + if len(authSplit) != 2 { + return "", fmt.Errorf(`unauthorized`) + } + + if strings.ToLower(authSplit[0]) != "bearer" { return "", fmt.Errorf(`not a bearer token`) } @@ -350,7 +355,12 @@ func GetIDToken(gc *gin.Context) (string, error) { return "", fmt.Errorf(`unauthorized`) } - if !strings.HasPrefix(auth, "Bearer ") { + authSplit := strings.Split(auth, " ") + if len(authSplit) != 2 { + return "", fmt.Errorf(`unauthorized`) + } + + if strings.ToLower(authSplit[0]) != "bearer" { return "", fmt.Errorf(`not a bearer token`) } diff --git a/server/token/jwt.go b/server/token/jwt.go index 90f6333..0b87c09 100644 --- a/server/token/jwt.go +++ b/server/token/jwt.go @@ -105,3 +105,59 @@ func ParseJWTToken(token, hostname, nonce, subject string) (jwt.MapClaims, error return claims, nil } + +// ParseJWTTokenWithoutNonce common util to parse jwt token without nonce +// used to validate ID token as it is not persisted in store +func ParseJWTTokenWithoutNonce(token, hostname string) (jwt.MapClaims, error) { + jwtType := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtType) + signingMethod := jwt.GetSigningMethod(jwtType) + + var err error + var claims jwt.MapClaims + + switch signingMethod { + case jwt.SigningMethodHS256, jwt.SigningMethodHS384, jwt.SigningMethodHS512: + _, err = jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) { + return []byte(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtSecret)), nil + }) + case jwt.SigningMethodRS256, jwt.SigningMethodRS384, jwt.SigningMethodRS512: + _, err = jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) { + key, err := crypto.ParseRsaPublicKeyFromPemStr(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey)) + if err != nil { + return nil, err + } + return key, nil + }) + case jwt.SigningMethodES256, jwt.SigningMethodES384, jwt.SigningMethodES512: + _, err = jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) { + key, err := crypto.ParseEcdsaPublicKeyFromPemStr(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey)) + if err != nil { + return nil, err + } + return key, nil + }) + default: + err = errors.New("unsupported signing method") + } + if err != nil { + return claims, err + } + + // claim parses exp & iat into float 64 with e^10, + // but we expect it to be int64 + // hence we need to assert interface and convert to int64 + intExp := int64(claims["exp"].(float64)) + intIat := int64(claims["iat"].(float64)) + claims["exp"] = intExp + claims["iat"] = intIat + + if claims["aud"] != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) { + return claims, errors.New("invalid audience") + } + + if claims["iss"] != hostname { + return claims, errors.New("invalid issuer") + } + + return claims, nil +} diff --git a/server/utils/common.go b/server/utils/common.go index d4a8d51..6835806 100644 --- a/server/utils/common.go +++ b/server/utils/common.go @@ -2,6 +2,7 @@ package utils import ( "log" + "reflect" "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db/models" @@ -47,3 +48,24 @@ func RemoveDuplicateString(strSlice []string) []string { } return list } + +// ConvertInterfaceToSlice to convert interface to slice interface +func ConvertInterfaceToSlice(slice interface{}) []interface{} { + s := reflect.ValueOf(slice) + if s.Kind() != reflect.Slice { + return nil + } + + // Keep the distinction between nil and empty slice input + if s.IsNil() { + return nil + } + + ret := make([]interface{}, s.Len()) + + for i := 0; i < s.Len(); i++ { + ret[i] = s.Index(i).Interface() + } + + return ret +} From b2541c8e9a95cf752ccba7fc141420fb577c9bde Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Thu, 24 Mar 2022 14:13:55 +0530 Subject: [PATCH 02/18] feat: update user access (#151) * feat: update user access * revoked timestamp field updated * updates * updates * updates --- dashboard/src/graphql/mutation/index.ts | 16 ++ dashboard/src/graphql/queries/index.ts | 1 + dashboard/src/pages/Users.tsx | 103 ++++++++++- server/db/models/user.go | 3 + server/graph/generated/generated.go | 229 ++++++++++++++++++++++++ server/graph/model/models_gen.go | 5 + server/graph/schema.graphqls | 7 + server/graph/schema.resolvers.go | 14 +- server/handlers/oauth_callback.go | 5 +- server/resolvers/enable_access.go | 44 +++++ server/resolvers/login.go | 4 + server/resolvers/magic_link_login.go | 4 + server/resolvers/revoke_access.go | 49 +++++ 13 files changed, 477 insertions(+), 7 deletions(-) create mode 100644 server/resolvers/enable_access.go create mode 100644 server/resolvers/revoke_access.go diff --git a/dashboard/src/graphql/mutation/index.ts b/dashboard/src/graphql/mutation/index.ts index df5db93..dcadcc3 100644 --- a/dashboard/src/graphql/mutation/index.ts +++ b/dashboard/src/graphql/mutation/index.ts @@ -53,3 +53,19 @@ export const InviteMembers = ` } } `; + +export const RevokeAccess = ` + mutation revokeAccess($param: UpdateAccessInput!) { + _revoke_access(param: $param) { + message + } + } +`; + +export const EnableAccess = ` + mutation revokeAccess($param: UpdateAccessInput!) { + _enable_access(param: $param) { + message + } + } +`; diff --git a/dashboard/src/graphql/queries/index.ts b/dashboard/src/graphql/queries/index.ts index fe35528..a7d0142 100644 --- a/dashboard/src/graphql/queries/index.ts +++ b/dashboard/src/graphql/queries/index.ts @@ -81,6 +81,7 @@ export const UserDetailsQuery = ` signup_methods roles created_at + revoked_timestamp } } } diff --git a/dashboard/src/pages/Users.tsx b/dashboard/src/pages/Users.tsx index a231117..6da5c83 100644 --- a/dashboard/src/pages/Users.tsx +++ b/dashboard/src/pages/Users.tsx @@ -39,7 +39,7 @@ import { FaAngleDown, } from 'react-icons/fa'; import { EmailVerificationQuery, UserDetailsQuery } from '../graphql/queries'; -import { UpdateUser } from '../graphql/mutation'; +import { EnableAccess, RevokeAccess, UpdateUser } from '../graphql/mutation'; import EditUserModal from '../components/EditUserModal'; import DeleteUserModal from '../components/DeleteUserModal'; import InviteMembersModal from '../components/InviteMembersModal'; @@ -67,6 +67,12 @@ interface userDataTypes { signup_methods: string; roles: [string]; created_at: number; + revoked_timestamp: number; +} + +const enum updateAccessActions { + REVOKE = 'REVOKE', + ENABLE = 'ENABLE', } const getMaxPages = (pagination: paginationPropTypes) => { @@ -185,6 +191,66 @@ export default function Users() { updateUserList(); }; + const updateAccessHandler = async ( + id: string, + action: updateAccessActions + ) => { + switch (action) { + case updateAccessActions.ENABLE: + const enableAccessRes = await client + .mutation(EnableAccess, { + param: { + user_id: id, + }, + }) + .toPromise(); + if (enableAccessRes.error) { + toast({ + title: 'User access enable failed', + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + } else { + toast({ + title: 'User access enabled successfully', + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + } + updateUserList(); + break; + case updateAccessActions.REVOKE: + const revokeAccessRes = await client + .mutation(RevokeAccess, { + param: { + user_id: id, + }, + }) + .toPromise(); + if (revokeAccessRes.error) { + toast({ + title: 'User access revoke failed', + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + } else { + toast({ + title: 'User access revoked successfully', + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + } + updateUserList(); + break; + default: + break; + } + }; + return ( @@ -206,6 +272,7 @@ export default function Users() { Signup Methods Roles Verified + Access Actions @@ -214,7 +281,7 @@ export default function Users() { const { email_verified, created_at, ...rest }: any = user; return ( - {user.email} + {user.email} {dayjs(user.created_at * 1000).format('MMM DD, YYYY')} @@ -229,6 +296,15 @@ export default function Users() { {user.email_verified.toString()} + + + {user.revoked_timestamp ? 'Revoked' : 'Enabled'} + + @@ -258,6 +334,29 @@ export default function Users() { user={rest} updateUserList={updateUserList} /> + {user.revoked_timestamp ? ( + + updateAccessHandler( + user.id, + updateAccessActions.ENABLE + ) + } + > + Enable Access + + ) : ( + + updateAccessHandler( + user.id, + updateAccessActions.REVOKE + ) + } + > + Revoke Access + + )} diff --git a/server/db/models/user.go b/server/db/models/user.go index b4e9dfd..d07486a 100644 --- a/server/db/models/user.go +++ b/server/db/models/user.go @@ -27,6 +27,7 @@ type User struct { Roles string `json:"roles" bson:"roles"` UpdatedAt int64 `json:"updated_at" bson:"updated_at"` CreatedAt int64 `json:"created_at" bson:"created_at"` + RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp"` } func (user *User) AsAPIUser() *model.User { @@ -35,6 +36,7 @@ func (user *User) AsAPIUser() *model.User { email := user.Email createdAt := user.CreatedAt updatedAt := user.UpdatedAt + revokedTimestamp := user.RevokedTimestamp return &model.User{ ID: user.ID, Email: user.Email, @@ -53,5 +55,6 @@ func (user *User) AsAPIUser() *model.User { Roles: strings.Split(user.Roles, ","), CreatedAt: &createdAt, UpdatedAt: &updatedAt, + RevokedTimestamp: revokedTimestamp, } } diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 9d5300e..70b0e76 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -115,6 +115,7 @@ type ComplexityRoot struct { AdminLogout func(childComplexity int) int AdminSignup func(childComplexity int, params model.AdminSignupInput) int DeleteUser func(childComplexity int, params model.DeleteUserInput) int + EnableAccess func(childComplexity int, param model.UpdateAccessInput) int ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int InviteMembers func(childComplexity int, params model.InviteMemberInput) int Login func(childComplexity int, params model.LoginInput) int @@ -123,6 +124,7 @@ type ComplexityRoot struct { ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int ResetPassword func(childComplexity int, params model.ResetPasswordInput) int Revoke func(childComplexity int, params model.OAuthRevokeInput) int + RevokeAccess func(childComplexity int, param model.UpdateAccessInput) int Signup func(childComplexity int, params model.SignUpInput) int UpdateEnv func(childComplexity int, params model.UpdateEnvInput) int UpdateProfile func(childComplexity int, params model.UpdateProfileInput) int @@ -167,6 +169,7 @@ type ComplexityRoot struct { PhoneNumberVerified func(childComplexity int) int Picture func(childComplexity int) int PreferredUsername func(childComplexity int) int + RevokedTimestamp func(childComplexity int) int Roles func(childComplexity int) int SignupMethods func(childComplexity int) int UpdatedAt func(childComplexity int) int @@ -217,6 +220,8 @@ type MutationResolver interface { AdminLogout(ctx context.Context) (*model.Response, error) UpdateEnv(ctx context.Context, params model.UpdateEnvInput) (*model.Response, error) InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) + RevokeAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) + EnableAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) } type QueryResolver interface { Meta(ctx context.Context) (*model.Meta, error) @@ -672,6 +677,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DeleteUser(childComplexity, args["params"].(model.DeleteUserInput)), true + case "Mutation._enable_access": + if e.complexity.Mutation.EnableAccess == nil { + break + } + + args, err := ec.field_Mutation__enable_access_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.EnableAccess(childComplexity, args["param"].(model.UpdateAccessInput)), true + case "Mutation.forgot_password": if e.complexity.Mutation.ForgotPassword == nil { break @@ -763,6 +780,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.Revoke(childComplexity, args["params"].(model.OAuthRevokeInput)), true + case "Mutation._revoke_access": + if e.complexity.Mutation.RevokeAccess == nil { + break + } + + args, err := ec.field_Mutation__revoke_access_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.RevokeAccess(childComplexity, args["param"].(model.UpdateAccessInput)), true + case "Mutation.signup": if e.complexity.Mutation.Signup == nil { break @@ -1032,6 +1061,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.User.PreferredUsername(childComplexity), true + case "User.revoked_timestamp": + if e.complexity.User.RevokedTimestamp == nil { + break + } + + return e.complexity.User.RevokedTimestamp(childComplexity), true + case "User.roles": if e.complexity.User.Roles == nil { break @@ -1260,6 +1296,7 @@ type User { roles: [String!]! created_at: Int64 updated_at: Int64 + revoked_timestamp: Int64 } type Users { @@ -1502,6 +1539,10 @@ input InviteMemberInput { redirect_uri: String } +input UpdateAccessInput { + user_id: String! +} + input ValidateJWTTokenInput { token_type: String! token: String! @@ -1527,6 +1568,8 @@ type Mutation { _admin_logout: Response! _update_env(params: UpdateEnvInput!): Response! _invite_members(params: InviteMemberInput!): Response! + _revoke_access(param: UpdateAccessInput!): Response! + _enable_access(param: UpdateAccessInput!): Response! } type Query { @@ -1593,6 +1636,21 @@ func (ec *executionContext) field_Mutation__delete_user_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Mutation__enable_access_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.UpdateAccessInput + if tmp, ok := rawArgs["param"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("param")) + arg0, err = ec.unmarshalNUpdateAccessInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUpdateAccessInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["param"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation__invite_members_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1608,6 +1666,21 @@ func (ec *executionContext) field_Mutation__invite_members_args(ctx context.Cont return args, nil } +func (ec *executionContext) field_Mutation__revoke_access_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.UpdateAccessInput + if tmp, ok := rawArgs["param"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("param")) + arg0, err = ec.unmarshalNUpdateAccessInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUpdateAccessInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["param"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation__update_env_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4397,6 +4470,90 @@ func (ec *executionContext) _Mutation__invite_members(ctx context.Context, field return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation__revoke_access(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation__revoke_access_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().RevokeAccess(rctx, args["param"].(model.UpdateAccessInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.Response) + fc.Result = res + return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) +} + +func (ec *executionContext) _Mutation__enable_access(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation__enable_access_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().EnableAccess(rctx, args["param"].(model.UpdateAccessInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.Response) + fc.Result = res + return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) +} + func (ec *executionContext) _Pagination_limit(ctx context.Context, field graphql.CollectedField, obj *model.Pagination) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5510,6 +5667,38 @@ func (ec *executionContext) _User_updated_at(ctx context.Context, field graphql. return ec.marshalOInt642ᚖint64(ctx, field.Selections, res) } +func (ec *executionContext) _User_revoked_timestamp(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "User", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.RevokedTimestamp, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int64) + fc.Result = res + return ec.marshalOInt642ᚖint64(ctx, field.Selections, res) +} + func (ec *executionContext) _Users_pagination(ctx context.Context, field graphql.CollectedField, obj *model.Users) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -7644,6 +7833,29 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i return it, nil } +func (ec *executionContext) unmarshalInputUpdateAccessInput(ctx context.Context, obj interface{}) (model.UpdateAccessInput, error) { + var it model.UpdateAccessInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "user_id": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("user_id")) + it.UserID, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, obj interface{}) (model.UpdateEnvInput, error) { var it model.UpdateEnvInput asMap := map[string]interface{}{} @@ -8572,6 +8784,16 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "_revoke_access": + out.Values[i] = ec._Mutation__revoke_access(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } + case "_enable_access": + out.Values[i] = ec._Mutation__enable_access(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -8854,6 +9076,8 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = ec._User_created_at(ctx, field, obj) case "updated_at": out.Values[i] = ec._User_updated_at(ctx, field, obj) + case "revoked_timestamp": + out.Values[i] = ec._User_revoked_timestamp(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -9466,6 +9690,11 @@ func (ec *executionContext) marshalNString2ᚕstringᚄ(ctx context.Context, sel return ret } +func (ec *executionContext) unmarshalNUpdateAccessInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUpdateAccessInput(ctx context.Context, v interface{}) (model.UpdateAccessInput, error) { + res, err := ec.unmarshalInputUpdateAccessInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNUpdateEnvInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUpdateEnvInput(ctx context.Context, v interface{}) (model.UpdateEnvInput, error) { res, err := ec.unmarshalInputUpdateEnvInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index ec260cf..ecb54c4 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -164,6 +164,10 @@ type SignUpInput struct { RedirectURI *string `json:"redirect_uri"` } +type UpdateAccessInput struct { + UserID string `json:"user_id"` +} + type UpdateEnvInput struct { AdminSecret *string `json:"ADMIN_SECRET"` CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"` @@ -249,6 +253,7 @@ type User struct { Roles []string `json:"roles"` CreatedAt *int64 `json:"created_at"` UpdatedAt *int64 `json:"updated_at"` + RevokedTimestamp *int64 `json:"revoked_timestamp"` } type Users struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 65cdf67..92af52f 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -43,6 +43,7 @@ type User { roles: [String!]! created_at: Int64 updated_at: Int64 + revoked_timestamp: Int64 } type Users { @@ -285,6 +286,10 @@ input InviteMemberInput { redirect_uri: String } +input UpdateAccessInput { + user_id: String! +} + input ValidateJWTTokenInput { token_type: String! token: String! @@ -310,6 +315,8 @@ type Mutation { _admin_logout: Response! _update_env(params: UpdateEnvInput!): Response! _invite_members(params: InviteMemberInput!): Response! + _revoke_access(param: UpdateAccessInput!): Response! + _enable_access(param: UpdateAccessInput!): Response! } type Query { diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 5b8501c..33f2055 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -79,6 +79,14 @@ func (r *mutationResolver) InviteMembers(ctx context.Context, params model.Invit return resolvers.InviteMembersResolver(ctx, params) } +func (r *mutationResolver) RevokeAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) { + return resolvers.RevokeAccessResolver(ctx, param) +} + +func (r *mutationResolver) EnableAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) { + return resolvers.EnableAccessResolver(ctx, param) +} + func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) { return resolvers.MetaResolver(ctx) } @@ -117,7 +125,5 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } -type ( - mutationResolver struct{ *Resolver } - queryResolver struct{ *Resolver } -) +type mutationResolver struct{ *Resolver } +type queryResolver struct{ *Resolver } diff --git a/server/handlers/oauth_callback.go b/server/handlers/oauth_callback.go index 0c6ffd5..c9563a6 100644 --- a/server/handlers/oauth_callback.go +++ b/server/handlers/oauth_callback.go @@ -95,9 +95,12 @@ func OAuthCallbackHandler() gin.HandlerFunc { user.EmailVerifiedAt = &now user, _ = db.Provider.AddUser(user) } else { + if user.RevokedTimestamp != nil { + c.JSON(400, gin.H{"error": "user access has been revoked"}) + } + // user exists in db, check if method was google // if not append google to existing signup method and save it - signupMethod := existingUser.SignupMethods if !strings.Contains(signupMethod, provider) { signupMethod = signupMethod + "," + provider diff --git a/server/resolvers/enable_access.go b/server/resolvers/enable_access.go new file mode 100644 index 0000000..647cada --- /dev/null +++ b/server/resolvers/enable_access.go @@ -0,0 +1,44 @@ +package resolvers + +import ( + "context" + "fmt" + "log" + + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" +) + +// EnableAccessResolver is a resolver for enabling user access +func EnableAccessResolver(ctx context.Context, params model.UpdateAccessInput) (*model.Response, error) { + gc, err := utils.GinContextFromContext(ctx) + var res *model.Response + if err != nil { + return res, err + } + + if !token.IsSuperAdmin(gc) { + return res, fmt.Errorf("unauthorized") + } + + user, err := db.Provider.GetUserByID(params.UserID) + if err != nil { + return res, err + } + + user.RevokedTimestamp = nil + + user, err = db.Provider.UpdateUser(user) + if err != nil { + log.Println("error updating user:", err) + return res, err + } + + res = &model.Response{ + Message: `user access enabled successfully`, + } + + return res, nil +} diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 355c77c..57f8158 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -35,6 +35,10 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes return res, fmt.Errorf(`user with this email not found`) } + if user.RevokedTimestamp != nil { + return res, fmt.Errorf(`user access has been revoked`) + } + if !strings.Contains(user.SignupMethods, constants.SignupMethodBasicAuth) { return res, fmt.Errorf(`user has not signed up email & password`) } diff --git a/server/resolvers/magic_link_login.go b/server/resolvers/magic_link_login.go index b69ea91..1c9d0bc 100644 --- a/server/resolvers/magic_link_login.go +++ b/server/resolvers/magic_link_login.go @@ -70,6 +70,10 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu // 2. user has not signed up for one of the available role but trying to signup. // Need to modify roles in this case + if user.RevokedTimestamp != nil { + return res, fmt.Errorf(`user access has been revoked`) + } + // find the unassigned roles if len(params.Roles) <= 0 { inputRoles = envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles) diff --git a/server/resolvers/revoke_access.go b/server/resolvers/revoke_access.go new file mode 100644 index 0000000..a470be8 --- /dev/null +++ b/server/resolvers/revoke_access.go @@ -0,0 +1,49 @@ +package resolvers + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/sessionstore" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" +) + +// RevokeAccessResolver is a resolver for revoking user access +func RevokeAccessResolver(ctx context.Context, params model.UpdateAccessInput) (*model.Response, error) { + gc, err := utils.GinContextFromContext(ctx) + var res *model.Response + if err != nil { + return res, err + } + + if !token.IsSuperAdmin(gc) { + return res, fmt.Errorf("unauthorized") + } + + user, err := db.Provider.GetUserByID(params.UserID) + if err != nil { + return res, err + } + + now := time.Now().Unix() + user.RevokedTimestamp = &now + + user, err = db.Provider.UpdateUser(user) + if err != nil { + log.Println("error updating user:", err) + return res, err + } + + go sessionstore.DeleteAllUserSession(fmt.Sprintf("%x", user.ID)) + + res = &model.Response{ + Message: `user access revoked successfully`, + } + + return res, nil +} From 4c4743ac24552ba5ad577e603ed21a2e47c2fdef Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Thu, 24 Mar 2022 18:23:22 +0530 Subject: [PATCH 03/18] generate-keys-modal added in dashboard --- .../src/components/GenerateKeysModal.tsx | 221 ++++++++++++++++++ .../src/components/InviteMembersModal.tsx | 1 - dashboard/src/constants.ts | 37 +++ dashboard/src/pages/Environment.tsx | 59 ++--- 4 files changed, 277 insertions(+), 41 deletions(-) create mode 100644 dashboard/src/components/GenerateKeysModal.tsx diff --git a/dashboard/src/components/GenerateKeysModal.tsx b/dashboard/src/components/GenerateKeysModal.tsx new file mode 100644 index 0000000..c5431df --- /dev/null +++ b/dashboard/src/components/GenerateKeysModal.tsx @@ -0,0 +1,221 @@ +import React from 'react'; +import { + Button, + Center, + Flex, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + useDisclosure, + Text, + useToast, + Input, +} from '@chakra-ui/react'; +import { useClient } from 'urql'; +import { FaSave } from 'react-icons/fa'; +import { + ECDSAEncryptionType, + envVarTypes, + HMACEncryptionType, + RSAEncryptionType, + SelectInputType, + TextAreaInputType, +} from '../constants'; +import InputField from './InputField'; + +interface propTypes { + saveEnvHandler: Function; + variables: envVarTypes; + setVariables: Function; +} + +interface stateVarTypes { + JWT_TYPE: string; + JWT_SECRET: string; + JWT_PRIVATE_KEY: string; + JWT_PUBLIC_KEY: string; +} + +const initState: stateVarTypes = { + JWT_TYPE: '', + JWT_SECRET: '', + JWT_PRIVATE_KEY: '', + JWT_PUBLIC_KEY: '', +}; + +const GenerateKeysModal = ({ + saveEnvHandler, + variables, + setVariables, +}: propTypes) => { + const client = useClient(); + const toast = useToast(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [stateVariables, setStateVariables] = React.useState({ + ...initState, + }); + React.useEffect(() => { + if (isOpen) { + setStateVariables({ ...initState, JWT_TYPE: variables.JWT_TYPE }); + } + }, [isOpen]); + const setKeys = () => { + // fetch keys from api + console.log('calling setKeys ==>> ', stateVariables.JWT_TYPE); + if (true) { + if (Object.values(HMACEncryptionType).includes(stateVariables.JWT_TYPE)) { + setStateVariables({ + ...stateVariables, + JWT_SECRET: 'hello_world', + JWT_PRIVATE_KEY: '', + JWT_PUBLIC_KEY: '', + }); + } else { + setStateVariables({ + ...stateVariables, + JWT_SECRET: '', + JWT_PRIVATE_KEY: 'test private key', + JWT_PUBLIC_KEY: 'test public key', + }); + } + toast({ + title: 'New keys generated', + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + } else { + toast({ + title: 'Error occurred generating keys', + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + closeHandler(); + } + }; + React.useEffect(() => { + if (isOpen) { + setKeys(); + } + }, [stateVariables.JWT_TYPE]); + const saveHandler = async () => { + setVariables({ ...variables, ...stateVariables }); + saveEnvHandler(); + closeHandler(); + }; + const closeHandler = async () => { + setStateVariables({ ...initState }); + onClose(); + }; + return ( + <> + + + + + New JWT keys + + + + + JWT Type: + + + + {Object.values(HMACEncryptionType).includes( + stateVariables.JWT_TYPE + ) ? ( + + + JWT Secret + +
+ + setStateVariables({ + ...stateVariables, + JWT_SECRET: event.target.value, + }) + } + /> +
+
+ ) : ( + <> + + + Public Key + +
+ +
+
+ + + Private Key + +
+ +
+
+ + )} +
+ + + + +
+
+ + ); +}; + +export default GenerateKeysModal; diff --git a/dashboard/src/components/InviteMembersModal.tsx b/dashboard/src/components/InviteMembersModal.tsx index bd3642d..8107669 100644 --- a/dashboard/src/components/InviteMembersModal.tsx +++ b/dashboard/src/components/InviteMembersModal.tsx @@ -26,7 +26,6 @@ import { import { useClient } from 'urql'; import { FaUserPlus, FaMinusCircle, FaPlus, FaUpload } from 'react-icons/fa'; import { useDropzone } from 'react-dropzone'; -import { escape } from 'lodash'; import { validateEmail, validateURI } from '../utils'; import { InviteMembers } from '../graphql/mutation'; import { ArrayInputOperations } from '../constants'; diff --git a/dashboard/src/constants.ts b/dashboard/src/constants.ts index 5fc9e5a..1ed810d 100644 --- a/dashboard/src/constants.ts +++ b/dashboard/src/constants.ts @@ -89,3 +89,40 @@ export const ECDSAEncryptionType = { ES384: 'ES384', ES512: 'ES512', }; + +export interface envVarTypes { + GOOGLE_CLIENT_ID: string; + GOOGLE_CLIENT_SECRET: string; + GITHUB_CLIENT_ID: string; + GITHUB_CLIENT_SECRET: string; + FACEBOOK_CLIENT_ID: string; + FACEBOOK_CLIENT_SECRET: string; + ROLES: [string] | []; + DEFAULT_ROLES: [string] | []; + PROTECTED_ROLES: [string] | []; + JWT_TYPE: string; + JWT_SECRET: string; + JWT_ROLE_CLAIM: string; + JWT_PRIVATE_KEY: string; + JWT_PUBLIC_KEY: string; + REDIS_URL: string; + SMTP_HOST: string; + SMTP_PORT: string; + SMTP_USERNAME: string; + SMTP_PASSWORD: string; + SENDER_EMAIL: string; + ALLOWED_ORIGINS: [string] | []; + ORGANIZATION_NAME: string; + ORGANIZATION_LOGO: string; + CUSTOM_ACCESS_TOKEN_SCRIPT: string; + ADMIN_SECRET: string; + DISABLE_LOGIN_PAGE: boolean; + DISABLE_MAGIC_LINK_LOGIN: boolean; + DISABLE_EMAIL_VERIFICATION: boolean; + DISABLE_BASIC_AUTHENTICATION: boolean; + DISABLE_SIGN_UP: boolean; + OLD_ADMIN_SECRET: string; + DATABASE_NAME: string; + DATABASE_TYPE: string; + DATABASE_URL: string; +} diff --git a/dashboard/src/pages/Environment.tsx b/dashboard/src/pages/Environment.tsx index f3df0e6..6ecf055 100644 --- a/dashboard/src/pages/Environment.tsx +++ b/dashboard/src/pages/Environment.tsx @@ -34,46 +34,11 @@ import { HMACEncryptionType, RSAEncryptionType, ECDSAEncryptionType, + envVarTypes, } from '../constants'; import { UpdateEnvVariables } from '../graphql/mutation'; import { getObjectDiff, capitalizeFirstLetter } from '../utils'; - -interface envVarTypes { - GOOGLE_CLIENT_ID: string; - GOOGLE_CLIENT_SECRET: string; - GITHUB_CLIENT_ID: string; - GITHUB_CLIENT_SECRET: string; - FACEBOOK_CLIENT_ID: string; - FACEBOOK_CLIENT_SECRET: string; - ROLES: [string] | []; - DEFAULT_ROLES: [string] | []; - PROTECTED_ROLES: [string] | []; - JWT_TYPE: string; - JWT_SECRET: string; - JWT_ROLE_CLAIM: string; - JWT_PRIVATE_KEY: string; - JWT_PUBLIC_KEY: string; - REDIS_URL: string; - SMTP_HOST: string; - SMTP_PORT: string; - SMTP_USERNAME: string; - SMTP_PASSWORD: string; - SENDER_EMAIL: string; - ALLOWED_ORIGINS: [string] | []; - ORGANIZATION_NAME: string; - ORGANIZATION_LOGO: string; - CUSTOM_ACCESS_TOKEN_SCRIPT: string; - ADMIN_SECRET: string; - DISABLE_LOGIN_PAGE: boolean; - DISABLE_MAGIC_LINK_LOGIN: boolean; - DISABLE_EMAIL_VERIFICATION: boolean; - DISABLE_BASIC_AUTHENTICATION: boolean; - DISABLE_SIGN_UP: boolean; - OLD_ADMIN_SECRET: string; - DATABASE_NAME: string; - DATABASE_TYPE: string; - DATABASE_URL: string; -} +import GenerateKeysModal from '../components/GenerateKeysModal'; export default function Environment() { const client = useClient(); @@ -410,9 +375,23 @@ export default function Environment() {
- - JWT (JSON Web Tokens) Configurations - + + + JWT (JSON Web Tokens) Configurations + + + + + From 90e2709eeb570e43b6392b73d6fbf9aa2ec35581 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 24 Mar 2022 19:21:52 +0530 Subject: [PATCH 04/18] feat: add mutation to generate new jwt secret & keys Resolves: #150 --- server/graph/generated/generated.go | 280 ++++++++++++++++++++++++++ server/graph/model/models_gen.go | 10 + server/graph/schema.graphqls | 11 + server/graph/schema.resolvers.go | 10 +- server/resolvers/generate_jwt_keys.go | 60 ++++++ server/test/generate_jwt_keys_test.go | 62 ++++++ 6 files changed, 431 insertions(+), 2 deletions(-) create mode 100644 server/resolvers/generate_jwt_keys.go create mode 100644 server/test/generate_jwt_keys_test.go diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 70b0e76..44523e8 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -98,6 +98,12 @@ type ComplexityRoot struct { Reason func(childComplexity int) int } + GenerateJWTKeysResponse struct { + PrivateKey func(childComplexity int) int + PublicKey func(childComplexity int) int + Secret func(childComplexity int) int + } + Meta struct { ClientID func(childComplexity int) int IsBasicAuthenticationEnabled func(childComplexity int) int @@ -117,6 +123,7 @@ type ComplexityRoot struct { DeleteUser func(childComplexity int, params model.DeleteUserInput) int EnableAccess func(childComplexity int, param model.UpdateAccessInput) int ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int + GenerateJwtKeys func(childComplexity int, params model.GenerateJWTKeysInput) int InviteMembers func(childComplexity int, params model.InviteMemberInput) int Login func(childComplexity int, params model.LoginInput) int Logout func(childComplexity int) int @@ -222,6 +229,7 @@ type MutationResolver interface { InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) RevokeAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) EnableAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) + GenerateJwtKeys(ctx context.Context, params model.GenerateJWTKeysInput) (*model.GenerateJWTKeysResponse, error) } type QueryResolver interface { Meta(ctx context.Context) (*model.Meta, error) @@ -571,6 +579,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Error.Reason(childComplexity), true + case "GenerateJWTKeysResponse.private_key": + if e.complexity.GenerateJWTKeysResponse.PrivateKey == nil { + break + } + + return e.complexity.GenerateJWTKeysResponse.PrivateKey(childComplexity), true + + case "GenerateJWTKeysResponse.public_key": + if e.complexity.GenerateJWTKeysResponse.PublicKey == nil { + break + } + + return e.complexity.GenerateJWTKeysResponse.PublicKey(childComplexity), true + + case "GenerateJWTKeysResponse.secret": + if e.complexity.GenerateJWTKeysResponse.Secret == nil { + break + } + + return e.complexity.GenerateJWTKeysResponse.Secret(childComplexity), true + case "Meta.client_id": if e.complexity.Meta.ClientID == nil { break @@ -701,6 +730,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.ForgotPassword(childComplexity, args["params"].(model.ForgotPasswordInput)), true + case "Mutation._generate_jwt_keys": + if e.complexity.Mutation.GenerateJwtKeys == nil { + break + } + + args, err := ec.field_Mutation__generate_jwt_keys_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.GenerateJwtKeys(childComplexity, args["params"].(model.GenerateJWTKeysInput)), true + case "Mutation._invite_members": if e.complexity.Mutation.InviteMembers == nil { break @@ -1384,6 +1425,12 @@ type ValidateJWTTokenResponse { is_valid: Boolean! } +type GenerateJWTKeysResponse { + secret: String + public_key: String + private_key: String +} + input UpdateEnvInput { ADMIN_SECRET: String CUSTOM_ACCESS_TOKEN_SCRIPT: String @@ -1549,6 +1596,10 @@ input ValidateJWTTokenInput { roles: [String!] } +input GenerateJWTKeysInput { + type: String! +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -1570,6 +1621,7 @@ type Mutation { _invite_members(params: InviteMemberInput!): Response! _revoke_access(param: UpdateAccessInput!): Response! _enable_access(param: UpdateAccessInput!): Response! + _generate_jwt_keys(params: GenerateJWTKeysInput!): GenerateJWTKeysResponse! } type Query { @@ -1651,6 +1703,21 @@ func (ec *executionContext) field_Mutation__enable_access_args(ctx context.Conte return args, nil } +func (ec *executionContext) field_Mutation__generate_jwt_keys_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.GenerateJWTKeysInput + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNGenerateJWTKeysInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐGenerateJWTKeysInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation__invite_members_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3455,6 +3522,102 @@ func (ec *executionContext) _Error_reason(ctx context.Context, field graphql.Col return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _GenerateJWTKeysResponse_secret(ctx context.Context, field graphql.CollectedField, obj *model.GenerateJWTKeysResponse) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "GenerateJWTKeysResponse", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Secret, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _GenerateJWTKeysResponse_public_key(ctx context.Context, field graphql.CollectedField, obj *model.GenerateJWTKeysResponse) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "GenerateJWTKeysResponse", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.PublicKey, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _GenerateJWTKeysResponse_private_key(ctx context.Context, field graphql.CollectedField, obj *model.GenerateJWTKeysResponse) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "GenerateJWTKeysResponse", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.PrivateKey, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + func (ec *executionContext) _Meta_version(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -4554,6 +4717,48 @@ func (ec *executionContext) _Mutation__enable_access(ctx context.Context, field return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation__generate_jwt_keys(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation__generate_jwt_keys_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().GenerateJwtKeys(rctx, args["params"].(model.GenerateJWTKeysInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.GenerateJWTKeysResponse) + fc.Result = res + return ec.marshalNGenerateJWTKeysResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐGenerateJWTKeysResponse(ctx, field.Selections, res) +} + func (ec *executionContext) _Pagination_limit(ctx context.Context, field graphql.CollectedField, obj *model.Pagination) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -7395,6 +7600,29 @@ func (ec *executionContext) unmarshalInputForgotPasswordInput(ctx context.Contex return it, nil } +func (ec *executionContext) unmarshalInputGenerateJWTKeysInput(ctx context.Context, obj interface{}) (model.GenerateJWTKeysInput, error) { + var it model.GenerateJWTKeysInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "type": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("type")) + it.Type, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputInviteMemberInput(ctx context.Context, obj interface{}) (model.InviteMemberInput, error) { var it model.InviteMemberInput asMap := map[string]interface{}{} @@ -8617,6 +8845,34 @@ func (ec *executionContext) _Error(ctx context.Context, sel ast.SelectionSet, ob return out } +var generateJWTKeysResponseImplementors = []string{"GenerateJWTKeysResponse"} + +func (ec *executionContext) _GenerateJWTKeysResponse(ctx context.Context, sel ast.SelectionSet, obj *model.GenerateJWTKeysResponse) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, generateJWTKeysResponseImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("GenerateJWTKeysResponse") + case "secret": + out.Values[i] = ec._GenerateJWTKeysResponse_secret(ctx, field, obj) + case "public_key": + out.Values[i] = ec._GenerateJWTKeysResponse_public_key(ctx, field, obj) + case "private_key": + out.Values[i] = ec._GenerateJWTKeysResponse_private_key(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var metaImplementors = []string{"Meta"} func (ec *executionContext) _Meta(ctx context.Context, sel ast.SelectionSet, obj *model.Meta) graphql.Marshaler { @@ -8794,6 +9050,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "_generate_jwt_keys": + out.Values[i] = ec._Mutation__generate_jwt_keys(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -9536,6 +9797,25 @@ func (ec *executionContext) unmarshalNForgotPasswordInput2githubᚗcomᚋauthori return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNGenerateJWTKeysInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐGenerateJWTKeysInput(ctx context.Context, v interface{}) (model.GenerateJWTKeysInput, error) { + res, err := ec.unmarshalInputGenerateJWTKeysInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNGenerateJWTKeysResponse2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐGenerateJWTKeysResponse(ctx context.Context, sel ast.SelectionSet, v model.GenerateJWTKeysResponse) graphql.Marshaler { + return ec._GenerateJWTKeysResponse(ctx, sel, &v) +} + +func (ec *executionContext) marshalNGenerateJWTKeysResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐGenerateJWTKeysResponse(ctx context.Context, sel ast.SelectionSet, v *model.GenerateJWTKeysResponse) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._GenerateJWTKeysResponse(ctx, sel, v) +} + func (ec *executionContext) unmarshalNID2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalID(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index ecb54c4..ea3b5bd 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -75,6 +75,16 @@ type ForgotPasswordInput struct { RedirectURI *string `json:"redirect_uri"` } +type GenerateJWTKeysInput struct { + Type string `json:"type"` +} + +type GenerateJWTKeysResponse struct { + Secret *string `json:"secret"` + PublicKey *string `json:"public_key"` + PrivateKey *string `json:"private_key"` +} + type InviteMemberInput struct { Emails []string `json:"emails"` RedirectURI *string `json:"redirect_uri"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 92af52f..a4f29f9 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -131,6 +131,12 @@ type ValidateJWTTokenResponse { is_valid: Boolean! } +type GenerateJWTKeysResponse { + secret: String + public_key: String + private_key: String +} + input UpdateEnvInput { ADMIN_SECRET: String CUSTOM_ACCESS_TOKEN_SCRIPT: String @@ -296,6 +302,10 @@ input ValidateJWTTokenInput { roles: [String!] } +input GenerateJWTKeysInput { + type: String! +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -317,6 +327,7 @@ type Mutation { _invite_members(params: InviteMemberInput!): Response! _revoke_access(param: UpdateAccessInput!): Response! _enable_access(param: UpdateAccessInput!): Response! + _generate_jwt_keys(params: GenerateJWTKeysInput!): GenerateJWTKeysResponse! } type Query { diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 33f2055..3f0444e 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -87,6 +87,10 @@ func (r *mutationResolver) EnableAccess(ctx context.Context, param model.UpdateA return resolvers.EnableAccessResolver(ctx, param) } +func (r *mutationResolver) GenerateJwtKeys(ctx context.Context, params model.GenerateJWTKeysInput) (*model.GenerateJWTKeysResponse, error) { + return resolvers.GenerateJWTKeysResolver(ctx, params) +} + func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) { return resolvers.MetaResolver(ctx) } @@ -125,5 +129,7 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } -type mutationResolver struct{ *Resolver } -type queryResolver struct{ *Resolver } +type ( + mutationResolver struct{ *Resolver } + queryResolver struct{ *Resolver } +) diff --git a/server/resolvers/generate_jwt_keys.go b/server/resolvers/generate_jwt_keys.go new file mode 100644 index 0000000..6c4c9e5 --- /dev/null +++ b/server/resolvers/generate_jwt_keys.go @@ -0,0 +1,60 @@ +package resolvers + +import ( + "context" + "fmt" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/crypto" + "github.com/authorizerdev/authorizer/server/envstore" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" +) + +// GenerateJWTKeysResolver mutation to generate new jwt keys +func GenerateJWTKeysResolver(ctx context.Context, params model.GenerateJWTKeysInput) (*model.GenerateJWTKeysResponse, error) { + gc, err := utils.GinContextFromContext(ctx) + if err != nil { + return nil, err + } + + if !token.IsSuperAdmin(gc) { + return nil, fmt.Errorf("unauthorized") + } + + clientID := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) + if crypto.IsHMACA(params.Type) { + secret, _, err := crypto.NewHMACKey(params.Type, clientID) + if err != nil { + return nil, err + } + return &model.GenerateJWTKeysResponse{ + Secret: &secret, + }, nil + } + + if crypto.IsRSA(params.Type) { + _, privateKey, publicKey, _, err := crypto.NewRSAKey(params.Type, clientID) + if err != nil { + return nil, err + } + return &model.GenerateJWTKeysResponse{ + PrivateKey: &privateKey, + PublicKey: &publicKey, + }, nil + } + + if crypto.IsECDSA(params.Type) { + _, privateKey, publicKey, _, err := crypto.NewECDSAKey(params.Type, clientID) + if err != nil { + return nil, err + } + return &model.GenerateJWTKeysResponse{ + PrivateKey: &privateKey, + PublicKey: &publicKey, + }, nil + } + + return nil, fmt.Errorf("invalid algorithm") +} diff --git a/server/test/generate_jwt_keys_test.go b/server/test/generate_jwt_keys_test.go new file mode 100644 index 0000000..b9acb76 --- /dev/null +++ b/server/test/generate_jwt_keys_test.go @@ -0,0 +1,62 @@ +package test + +import ( + "fmt" + "testing" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/crypto" + "github.com/authorizerdev/authorizer/server/envstore" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/resolvers" + "github.com/stretchr/testify/assert" +) + +func generateJWTkeyTest(t *testing.T, s TestSetup) { + t.Helper() + req, ctx := createContext(s) + t.Run(`generate_jwt_keys`, func(t *testing.T) { + t.Run(`should throw unauthorized`, func(t *testing.T) { + res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{ + Type: "HS256", + }) + assert.Error(t, err) + assert.Nil(t, res) + }) + t.Run(`should throw invalid`, func(t *testing.T) { + res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{ + Type: "test", + }) + assert.Error(t, err) + assert.Nil(t, res) + }) + h, err := crypto.EncryptPassword(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret)) + assert.Nil(t, err) + req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h)) + t.Run(`should generate HS256 secret`, func(t *testing.T) { + res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{ + Type: "HS256", + }) + assert.NoError(t, err) + assert.NotEmpty(t, res.Secret) + }) + + t.Run(`should generate RS256 secret`, func(t *testing.T) { + res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{ + Type: "RS256", + }) + assert.NoError(t, err) + assert.NotEmpty(t, res.PrivateKey) + assert.NotEmpty(t, res.PublicKey) + }) + + t.Run(`should generate ES256 secret`, func(t *testing.T) { + res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{ + Type: "ES256", + }) + assert.NoError(t, err) + assert.NotEmpty(t, res.PrivateKey) + assert.NotEmpty(t, res.PublicKey) + }) + }) +} From 7a18fc6312de03d3d0f841dacfa7c2a991aba6d3 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 24 Mar 2022 19:23:43 +0530 Subject: [PATCH 05/18] fix: add test to resolvers test --- server/test/resolvers_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/test/resolvers_test.go b/server/test/resolvers_test.go index b64695d..225f656 100644 --- a/server/test/resolvers_test.go +++ b/server/test/resolvers_test.go @@ -48,6 +48,7 @@ func TestResolvers(t *testing.T) { adminSessionTests(t, s) updateEnvTests(t, s) envTests(t, s) + generateJWTkeyTest(t, s) // user tests loginTests(t, s) From a3d9783aef53890f4598853aa193701fb0c75fdd Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Thu, 24 Mar 2022 21:08:10 +0530 Subject: [PATCH 06/18] mutation to update jwt vars added --- .../src/components/GenerateKeysModal.tsx | 98 ++++++++++--------- dashboard/src/graphql/mutation/index.ts | 10 ++ dashboard/src/pages/Environment.tsx | 45 ++++----- 3 files changed, 82 insertions(+), 71 deletions(-) diff --git a/dashboard/src/components/GenerateKeysModal.tsx b/dashboard/src/components/GenerateKeysModal.tsx index c5431df..c68a756 100644 --- a/dashboard/src/components/GenerateKeysModal.tsx +++ b/dashboard/src/components/GenerateKeysModal.tsx @@ -19,18 +19,17 @@ import { useClient } from 'urql'; import { FaSave } from 'react-icons/fa'; import { ECDSAEncryptionType, - envVarTypes, HMACEncryptionType, RSAEncryptionType, SelectInputType, TextAreaInputType, } from '../constants'; import InputField from './InputField'; +import { GenerateKeys, UpdateEnvVariables } from '../graphql/mutation'; interface propTypes { - saveEnvHandler: Function; - variables: envVarTypes; - setVariables: Function; + jwtType: string; + getData: Function; } interface stateVarTypes { @@ -47,11 +46,7 @@ const initState: stateVarTypes = { JWT_PUBLIC_KEY: '', }; -const GenerateKeysModal = ({ - saveEnvHandler, - variables, - setVariables, -}: propTypes) => { +const GenerateKeysModal = ({ jwtType, getData }: propTypes) => { const client = useClient(); const toast = useToast(); const { isOpen, onOpen, onClose } = useDisclosure(); @@ -60,17 +55,26 @@ const GenerateKeysModal = ({ }); React.useEffect(() => { if (isOpen) { - setStateVariables({ ...initState, JWT_TYPE: variables.JWT_TYPE }); + setStateVariables({ ...initState, JWT_TYPE: jwtType }); } }, [isOpen]); - const setKeys = () => { - // fetch keys from api - console.log('calling setKeys ==>> ', stateVariables.JWT_TYPE); - if (true) { - if (Object.values(HMACEncryptionType).includes(stateVariables.JWT_TYPE)) { + const fetchKeys = async () => { + const res = await client + .mutation(GenerateKeys, { params: { type: stateVariables.JWT_TYPE } }) + .toPromise(); + if (res?.error) { + toast({ + title: 'Error occurred generating jwt keys', + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + closeHandler(); + } else { + if (res?.data?._generate_jwt_keys?.secret) { setStateVariables({ ...stateVariables, - JWT_SECRET: 'hello_world', + JWT_SECRET: res.data._generate_jwt_keys.secret, JWT_PRIVATE_KEY: '', JWT_PUBLIC_KEY: '', }); @@ -78,39 +82,45 @@ const GenerateKeysModal = ({ setStateVariables({ ...stateVariables, JWT_SECRET: '', - JWT_PRIVATE_KEY: 'test private key', - JWT_PUBLIC_KEY: 'test public key', + JWT_PRIVATE_KEY: res.data._generate_jwt_keys.private_key, + JWT_PUBLIC_KEY: res.data._generate_jwt_keys.public_key, }); } + } + }; + React.useEffect(() => { + if (isOpen && stateVariables.JWT_TYPE) { + fetchKeys(); + } + }, [stateVariables.JWT_TYPE]); + const saveHandler = async () => { + const res = await client + .mutation(UpdateEnvVariables, { params: { ...stateVariables } }) + .toPromise(); + + if (res.error) { toast({ - title: 'New keys generated', - isClosable: true, - status: 'success', - position: 'bottom-right', - }); - } else { - toast({ - title: 'Error occurred generating keys', + title: 'Error occurred setting jwt keys', isClosable: true, status: 'error', position: 'bottom-right', }); - closeHandler(); + + return; } - }; - React.useEffect(() => { - if (isOpen) { - setKeys(); - } - }, [stateVariables.JWT_TYPE]); - const saveHandler = async () => { - setVariables({ ...variables, ...stateVariables }); - saveEnvHandler(); + toast({ + title: 'JWT keys updated successfully', + isClosable: true, + status: 'success', + position: 'bottom-right', + }); closeHandler(); }; - const closeHandler = async () => { + + const closeHandler = () => { setStateVariables({ ...initState }); onClose(); + getData(); }; return ( <> @@ -149,10 +159,10 @@ const GenerateKeysModal = ({ stateVariables.JWT_TYPE ) ? ( - + JWT Secret -
+
- + Public Key -
+
- + Private Key -
+
- + New JWT keys @@ -146,56 +160,67 @@ const GenerateKeysModal = ({ jwtType, getData }: propTypes) => { }} /> - {Object.values(HMACEncryptionType).includes( - stateVariables.JWT_TYPE - ) ? ( - - - JWT Secret - -
- - setStateVariables({ - ...stateVariables, - JWT_SECRET: event.target.value, - }) - } - /> -
-
+ {isLoading ? ( +
+ +
) : ( <> - - - Public Key + {Object.values(HMACEncryptionType).includes( + stateVariables.JWT_TYPE + ) ? ( + + + JWT Secret + +
+ + setStateVariables({ + ...stateVariables, + JWT_SECRET: event.target.value, + }) + } + readOnly + /> +
-
- -
-
- - - Private Key - -
- -
-
+ ) : ( + <> + + + Public Key + +
+ +
+
+ + + Private Key + +
+ +
+
+ + )} )} @@ -206,7 +231,7 @@ const GenerateKeysModal = ({ jwtType, getData }: propTypes) => { colorScheme="blue" variant="solid" onClick={saveHandler} - isDisabled={false} + isDisabled={isLoading} >
Apply From b1b43a41ca4d0069ddbdbb1f0f7e0f22dc701738 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 24 Mar 2022 22:19:30 +0530 Subject: [PATCH 10/18] fix: resetting the keys --- server/resolvers/update_env.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/resolvers/update_env.go b/server/resolvers/update_env.go index 0f428bb..8da29d7 100644 --- a/server/resolvers/update_env.go +++ b/server/resolvers/update_env.go @@ -53,11 +53,19 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model } if isJWTUpdated { + // use to reset when type is changed from rsa, edsa -> hmac or vice a versa + defaultSecret := "" + defaultPublicKey := "" + defaultPrivateKey := "" // check if jwt secret is provided if crypto.IsHMACA(algo) { if params.JwtSecret == nil { return res, fmt.Errorf("jwt secret is required for HMAC algorithm") } + + // reset public key and private key + params.JwtPrivateKey = &defaultPrivateKey + params.JwtPublicKey = &defaultPublicKey } if crypto.IsRSA(algo) { @@ -65,6 +73,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model return res, fmt.Errorf("jwt private and public key is required for RSA (PKCS1) / ECDSA algorithm") } + // reset the jwt secret + params.JwtSecret = &defaultSecret _, err = crypto.ParseRsaPrivateKeyFromPemStr(*params.JwtPrivateKey) if err != nil { return res, err @@ -81,6 +91,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model return res, fmt.Errorf("jwt private and public key is required for RSA (PKCS1) / ECDSA algorithm") } + // reset the jwt secret + params.JwtSecret = &defaultSecret _, err = crypto.ParseEcdsaPrivateKeyFromPemStr(*params.JwtPrivateKey) if err != nil { return res, err From 7e91c6ca28946be96d82e31e942a9940bb3b5d49 Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Thu, 24 Mar 2022 22:53:54 +0530 Subject: [PATCH 11/18] fix: update jwt keys persisting old values --- dashboard/src/pages/Environment.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dashboard/src/pages/Environment.tsx b/dashboard/src/pages/Environment.tsx index 352bde5..63929d3 100644 --- a/dashboard/src/pages/Environment.tsx +++ b/dashboard/src/pages/Environment.tsx @@ -187,6 +187,8 @@ export default function Environment() { disableInputField: true, }); + getData(); + toast({ title: `Successfully updated ${ Object.keys(updatedEnvVariables).length From 41b5f00b83ad80455b77d019cd06b4ae2df20dd1 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 25 Mar 2022 00:32:21 +0530 Subject: [PATCH 12/18] fix: client id make readonly --- dashboard/src/pages/Environment.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/src/pages/Environment.tsx b/dashboard/src/pages/Environment.tsx index 63929d3..b4fd091 100644 --- a/dashboard/src/pages/Environment.tsx +++ b/dashboard/src/pages/Environment.tsx @@ -215,7 +215,7 @@ export default function Environment() { setVariables={() => {}} inputType={TextInputType.CLIENT_ID} placeholder="Client ID" - isDisabled={true} + readOnly={true} />
@@ -231,7 +231,7 @@ export default function Environment() { setFieldVisibility={setFieldVisibility} inputType={HiddenInputType.CLIENT_SECRET} placeholder="Client Secret" - isDisabled={true} + readOnly={true} />
From 044b025ba2e34d582d2f19816c6599585b365fff Mon Sep 17 00:00:00 2001 From: "egor.medvedev" Date: Fri, 25 Mar 2022 15:21:20 +0300 Subject: [PATCH 13/18] enhancement: add access_token_expiry_time env variable --- dashboard/src/constants.ts | 1 + dashboard/src/graphql/queries/index.ts | 1 + dashboard/src/pages/Environment.tsx | 24 ++++++++++-- server/constants/env.go | 2 + server/env/env.go | 4 ++ server/graph/generated/generated.go | 52 ++++++++++++++++++++++++++ server/graph/model/models_gen.go | 2 + server/graph/schema.graphqls | 2 + server/handlers/authorize.go | 7 +++- server/handlers/oauth_callback.go | 7 +++- server/handlers/token.go | 7 +++- server/handlers/verify_email.go | 7 +++- server/resolvers/env.go | 2 + server/resolvers/login.go | 7 +++- server/resolvers/session.go | 7 +++- server/resolvers/signup.go | 5 ++- server/resolvers/verify_email.go | 6 ++- server/token/auth_token.go | 12 +++++- server/utils/parser.go | 21 +++++++++++ 19 files changed, 163 insertions(+), 13 deletions(-) create mode 100644 server/utils/parser.go diff --git a/dashboard/src/constants.ts b/dashboard/src/constants.ts index 64fa372..fb51cc4 100644 --- a/dashboard/src/constants.ts +++ b/dashboard/src/constants.ts @@ -2,6 +2,7 @@ export const LOGO_URL = 'https://user-images.githubusercontent.com/6964334/147834043-fc384cab-e7ca-40f8-9663-38fc25fd5f3a.png'; export const TextInputType = { + ACCESS_TOKEN_EXPIRY_TIME: 'ACCESS_TOKEN_EXPIRY_TIME', CLIENT_ID: 'CLIENT_ID', GOOGLE_CLIENT_ID: 'GOOGLE_CLIENT_ID', GITHUB_CLIENT_ID: 'GITHUB_CLIENT_ID', diff --git a/dashboard/src/graphql/queries/index.ts b/dashboard/src/graphql/queries/index.ts index 8528f3f..23f2adc 100644 --- a/dashboard/src/graphql/queries/index.ts +++ b/dashboard/src/graphql/queries/index.ts @@ -52,6 +52,7 @@ export const EnvVariablesQuery = ` DATABASE_NAME, DATABASE_TYPE, DATABASE_URL, + ACCESS_TOKEN_EXPIRY_TIME, } } `; diff --git a/dashboard/src/pages/Environment.tsx b/dashboard/src/pages/Environment.tsx index 06480bc..94fcf48 100644 --- a/dashboard/src/pages/Environment.tsx +++ b/dashboard/src/pages/Environment.tsx @@ -72,6 +72,7 @@ interface envVarTypes { DATABASE_NAME: string; DATABASE_TYPE: string; DATABASE_URL: string; + ACCESS_TOKEN_EXPIRY_TIME: string; } export default function Environment() { @@ -118,6 +119,7 @@ export default function Environment() { DATABASE_NAME: '', DATABASE_TYPE: '', DATABASE_URL: '', + ACCESS_TOKEN_EXPIRY_TIME: '', }); const [fieldVisibility, setFieldVisibility] = React.useState< @@ -626,19 +628,35 @@ export default function Environment() { - Custom Access Token Scripts + Access Token -
+ + Access Token Expiry Time: + + + + + + + Custom Access Token Scripts: + + + -
+
diff --git a/server/constants/env.go b/server/constants/env.go index 4206c37..273af91 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -21,6 +21,8 @@ const ( // EnvKeyPort key for env variable PORT EnvKeyPort = "PORT" + // EnvKeyAccessTokenExpiryTime key for env variable ACCESS_TOKEN_EXPIRY_TIME + EnvKeyAccessTokenExpiryTime = "ACCESS_TOKEN_EXPIRY_TIME" // EnvKeyAdminSecret key for env variable ADMIN_SECRET EnvKeyAdminSecret = "ADMIN_SECRET" // EnvKeyDatabaseType key for env variable DATABASE_TYPE diff --git a/server/env/env.go b/server/env/env.go index d430e2f..40905fb 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -120,6 +120,10 @@ func InitAllEnv() error { } } + if envData.StringEnv[constants.EnvKeyAccessTokenExpiryTime] == "" { + envData.StringEnv[constants.EnvKeyAccessTokenExpiryTime] = os.Getenv(constants.EnvKeyAccessTokenExpiryTime) + } + if envData.StringEnv[constants.EnvKeyAdminSecret] == "" { envData.StringEnv[constants.EnvKeyAdminSecret] = os.Getenv(constants.EnvKeyAdminSecret) } diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 817dd79..8e45791 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -53,6 +53,7 @@ type ComplexityRoot struct { } Env struct { + AccessTokenExpiryTime func(childComplexity int) int AdminSecret func(childComplexity int) int AllowedOrigins func(childComplexity int) int AppURL func(childComplexity int) int @@ -276,6 +277,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AuthResponse.User(childComplexity), true + case "Env.ACCESS_TOKEN_EXPIRY_TIME": + if e.complexity.Env.AccessTokenExpiryTime == nil { + break + } + + return e.complexity.Env.AccessTokenExpiryTime(childComplexity), true + case "Env.ADMIN_SECRET": if e.complexity.Env.AdminSecret == nil { break @@ -1247,6 +1255,7 @@ type Response { } type Env { + ACCESS_TOKEN_EXPIRY_TIME: String ADMIN_SECRET: String DATABASE_NAME: String! DATABASE_URL: String! @@ -1287,6 +1296,7 @@ type Env { } input UpdateEnvInput { + ACCESS_TOKEN_EXPIRY_TIME: String ADMIN_SECRET: String CUSTOM_ACCESS_TOKEN_SCRIPT: String OLD_ADMIN_SECRET: String @@ -1975,6 +1985,38 @@ func (ec *executionContext) _AuthResponse_user(ctx context.Context, field graphq return ec.marshalOUser2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUser(ctx, field.Selections, res) } +func (ec *executionContext) _Env_ACCESS_TOKEN_EXPIRY_TIME(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Env", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.AccessTokenExpiryTime, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + func (ec *executionContext) _Env_ADMIN_SECRET(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -7322,6 +7364,14 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob for k, v := range asMap { switch k { + case "ACCESS_TOKEN_EXPIRY_TIME": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ACCESS_TOKEN_EXPIRY_TIME")) + it.AccessTokenExpiryTime, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } case "ADMIN_SECRET": var err error @@ -7893,6 +7943,8 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Env") + case "ACCESS_TOKEN_EXPIRY_TIME": + out.Values[i] = ec._Env_ACCESS_TOKEN_EXPIRY_TIME(ctx, field, obj) case "ADMIN_SECRET": out.Values[i] = ec._Env_ADMIN_SECRET(ctx, field, obj) case "DATABASE_NAME": diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index ea069e5..01e323e 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -24,6 +24,7 @@ type DeleteUserInput struct { } type Env struct { + AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"` AdminSecret *string `json:"ADMIN_SECRET"` DatabaseName string `json:"DATABASE_NAME"` DatabaseURL string `json:"DATABASE_URL"` @@ -157,6 +158,7 @@ type SignUpInput struct { } type UpdateEnvInput struct { + AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"` AdminSecret *string `json:"ADMIN_SECRET"` CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"` OldAdminSecret *string `json:"OLD_ADMIN_SECRET"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 18a727c..9a0c175 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -85,6 +85,7 @@ type Response { } type Env { + ACCESS_TOKEN_EXPIRY_TIME: String ADMIN_SECRET: String DATABASE_NAME: String! DATABASE_URL: String! @@ -125,6 +126,7 @@ type Env { } input UpdateEnvInput { + ACCESS_TOKEN_EXPIRY_TIME: String ADMIN_SECRET: String CUSTOM_ACCESS_TOKEN_SCRIPT: String OLD_ADMIN_SECRET: String diff --git a/server/handlers/authorize.go b/server/handlers/authorize.go index 53c94ee..572ebae 100644 --- a/server/handlers/authorize.go +++ b/server/handlers/authorize.go @@ -4,6 +4,7 @@ import ( "net/http" "strconv" "strings" + "time" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie" @@ -276,7 +277,11 @@ func AuthorizeHandler() gin.HandlerFunc { sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) cookie.SetSession(gc, authToken.FingerPrintHash) - expiresIn := int64(1800) + + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } // used of query mode params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token diff --git a/server/handlers/oauth_callback.go b/server/handlers/oauth_callback.go index 1d28234..6a03ad4 100644 --- a/server/handlers/oauth_callback.go +++ b/server/handlers/oauth_callback.go @@ -150,7 +150,12 @@ func OAuthCallbackHandler() gin.HandlerFunc { if err != nil { c.JSON(500, gin.H{"error": err.Error()}) } - expiresIn := int64(1800) + + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + stateValue + "&id_token=" + authToken.IDToken.Token cookie.SetSession(c, authToken.FingerPrintHash) diff --git a/server/handlers/token.go b/server/handlers/token.go index 45c66e7..7e1f12d 100644 --- a/server/handlers/token.go +++ b/server/handlers/token.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "net/http" "strings" + "time" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie" @@ -174,7 +175,11 @@ func TokenHandler() gin.HandlerFunc { sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) cookie.SetSession(gc, authToken.FingerPrintHash) - expiresIn := int64(1800) + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + res := map[string]interface{}{ "access_token": authToken.AccessToken.Token, "id_token": authToken.IDToken.Token, diff --git a/server/handlers/verify_email.go b/server/handlers/verify_email.go index 80fe6ad..6333620 100644 --- a/server/handlers/verify_email.go +++ b/server/handlers/verify_email.go @@ -82,7 +82,12 @@ func VerifyEmailHandler() gin.HandlerFunc { c.JSON(500, errorRes) return } - expiresIn := int64(1800) + + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token cookie.SetSession(c, authToken.FingerPrintHash) diff --git a/server/resolvers/env.go b/server/resolvers/env.go index 623d6b3..c834919 100644 --- a/server/resolvers/env.go +++ b/server/resolvers/env.go @@ -27,6 +27,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) { // get clone of store store := envstore.EnvStoreObj.GetEnvStoreClone() + accessTokenExpiryTime := store.StringEnv[constants.EnvKeyAccessTokenExpiryTime] adminSecret := store.StringEnv[constants.EnvKeyAdminSecret] clientID := store.StringEnv[constants.EnvKeyClientID] clientSecret := store.StringEnv[constants.EnvKeyClientSecret] @@ -66,6 +67,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) { organizationLogo := store.StringEnv[constants.EnvKeyOrganizationLogo] res = &model.Env{ + AccessTokenExpiryTime: &accessTokenExpiryTime, AdminSecret: &adminSecret, DatabaseName: databaseName, DatabaseURL: databaseURL, diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 355c77c..1ae38b3 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "strings" + "time" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie" @@ -69,7 +70,11 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes return res, err } - expiresIn := int64(1800) + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + res = &model.AuthResponse{ Message: `Logged in successfully`, AccessToken: &authToken.AccessToken.Token, diff --git a/server/resolvers/session.go b/server/resolvers/session.go index 151321d..e130130 100644 --- a/server/resolvers/session.go +++ b/server/resolvers/session.go @@ -3,6 +3,7 @@ package resolvers import ( "context" "fmt" + "time" "github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/db" @@ -69,7 +70,11 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) cookie.SetSession(gc, authToken.FingerPrintHash) - expiresIn := int64(1800) + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + res = &model.AuthResponse{ Message: `Session token refreshed`, AccessToken: &authToken.AccessToken.Token, diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index 308d284..669f053 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -164,7 +164,10 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR cookie.SetSession(gc, authToken.FingerPrintHash) go utils.SaveSessionInDB(gc, user.ID) - expiresIn := int64(1800) + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } res = &model.AuthResponse{ Message: `Signed up successfully.`, diff --git a/server/resolvers/verify_email.go b/server/resolvers/verify_email.go index fe13c9a..65e8494 100644 --- a/server/resolvers/verify_email.go +++ b/server/resolvers/verify_email.go @@ -64,7 +64,11 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m cookie.SetSession(gc, authToken.FingerPrintHash) go utils.SaveSessionInDB(gc, user.ID) - expiresIn := int64(1800) + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + res = &model.AuthResponse{ Message: `Email verified successfully.`, AccessToken: &authToken.AccessToken.Token, diff --git a/server/token/auth_token.go b/server/token/auth_token.go index 350da17..fac8273 100644 --- a/server/token/auth_token.go +++ b/server/token/auth_token.go @@ -130,7 +130,11 @@ func CreateRefreshToken(user models.User, roles, scopes []string, hostname, nonc // CreateAccessToken util to create JWT token, based on // user information, roles config and CUSTOM_ACCESS_TOKEN_SCRIPT func CreateAccessToken(user models.User, roles, scopes []string, hostName, nonce string) (string, int64, error) { - expiryBound := time.Minute * 30 + expiryBound, err := utils.ParseDurationInSeconds(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAccessTokenExpiryTime)) + if err != nil { + expiryBound = time.Minute * 15 + } + expiresAt := time.Now().Add(expiryBound).Unix() customClaims := jwt.MapClaims{ @@ -277,7 +281,11 @@ func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionD // CreateIDToken util to create JWT token, based on // user information, roles config and CUSTOM_ACCESS_TOKEN_SCRIPT func CreateIDToken(user models.User, roles []string, hostname, nonce string) (string, int64, error) { - expiryBound := time.Minute * 30 + expiryBound, err := utils.ParseDurationInSeconds(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAccessTokenExpiryTime)) + if err != nil { + expiryBound = time.Minute * 15 + } + expiresAt := time.Now().Add(expiryBound).Unix() resUser := user.AsAPIUser() diff --git a/server/utils/parser.go b/server/utils/parser.go new file mode 100644 index 0000000..1b037c0 --- /dev/null +++ b/server/utils/parser.go @@ -0,0 +1,21 @@ +package utils + +import ( + "errors" + "time" +) + +// ParseDurationInSeconds parses input s, removes ms/us/ns and returns result duration +func ParseDurationInSeconds(s string) (time.Duration, error) { + d, err := time.ParseDuration(s) + if err != nil { + return 0, err + } + + d = d.Truncate(time.Second) + if d <= 0 { + return 0, errors.New(`duration must be greater than 0s`) + } + + return d, nil +} From 4a3e3633ea77e56923c979b9c2009fd5aaeea044 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 25 Mar 2022 20:29:00 +0530 Subject: [PATCH 14/18] fix: default access token expiry time --- dashboard/src/pages/Environment.tsx | 7 ++++--- server/env/env.go | 3 +++ server/env/persist_env.go | 1 + server/handlers/authorize.go | 3 --- server/resolvers/env.go | 4 ++++ server/token/auth_token.go | 4 ++-- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/dashboard/src/pages/Environment.tsx b/dashboard/src/pages/Environment.tsx index c51b95a..a6ef5a5 100644 --- a/dashboard/src/pages/Environment.tsx +++ b/dashboard/src/pages/Environment.tsx @@ -618,11 +618,12 @@ export default function Environment() { - - Custom Access Token Scripts: + + Custom Scripts: + Used to add custom fields in ID token - redirect URI:", redirectURI) - fmt.Println("=> state:", state) if redirectURI == "" { redirectURI = "/app" } diff --git a/server/resolvers/env.go b/server/resolvers/env.go index 9ee3c60..dc7db8d 100644 --- a/server/resolvers/env.go +++ b/server/resolvers/env.go @@ -67,6 +67,10 @@ func EnvResolver(ctx context.Context) (*model.Env, error) { organizationName := store.StringEnv[constants.EnvKeyOrganizationName] organizationLogo := store.StringEnv[constants.EnvKeyOrganizationLogo] + if accessTokenExpiryTime == "" { + accessTokenExpiryTime = "30m" + } + res = &model.Env{ AccessTokenExpiryTime: &accessTokenExpiryTime, AdminSecret: &adminSecret, diff --git a/server/token/auth_token.go b/server/token/auth_token.go index 905df4a..9608809 100644 --- a/server/token/auth_token.go +++ b/server/token/auth_token.go @@ -132,7 +132,7 @@ func CreateRefreshToken(user models.User, roles, scopes []string, hostname, nonc func CreateAccessToken(user models.User, roles, scopes []string, hostName, nonce string) (string, int64, error) { expiryBound, err := utils.ParseDurationInSeconds(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAccessTokenExpiryTime)) if err != nil { - expiryBound = time.Minute * 15 + expiryBound = time.Minute * 30 } expiresAt := time.Now().Add(expiryBound).Unix() @@ -288,7 +288,7 @@ func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionD func CreateIDToken(user models.User, roles []string, hostname, nonce string) (string, int64, error) { expiryBound, err := utils.ParseDurationInSeconds(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAccessTokenExpiryTime)) if err != nil { - expiryBound = time.Minute * 15 + expiryBound = time.Minute * 30 } expiresAt := time.Now().Add(expiryBound).Unix() From fe73c2f6f8f8cdd34cfcaa590b95c55fe071e39f Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sat, 26 Mar 2022 07:00:01 +0530 Subject: [PATCH 15/18] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 59cc6c0..1d34920 100644 --- a/README.md +++ b/README.md @@ -26,18 +26,16 @@ - ✅ Sign-in / Sign-up with email ID and password - ✅ Secure session management - ✅ Email verification +- ✅ OAuth2 and OpenID compatible APIs - ✅ APIs to update profile securely - ✅ Forgot password flow using email - ✅ Social logins (Google, Github, Facebook, more coming soon) - ✅ Role-based access management -- ✅ Password-less login with email and magic link +- ✅ Password-less login with magic link login ## Roadmap -- Support more JWT encryption algorithms (Currently supporting HS256) - 2 Factor authentication -- Back office (Admin dashboard to manage user) -- Support more database - VueJS SDK - Svelte SDK - React Native SDK From 4fa9f79c3fdf4a04187fe62f69cf1e517ffe9ced Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Wed, 30 Mar 2022 11:50:22 +0530 Subject: [PATCH 16/18] fix: setting the cookie for proxy setup --- app/package-lock.json | 30 +++++++++++++++--------------- app/package.json | 2 +- dashboard/src/App.tsx | 3 +++ server/constants/env.go | 1 - server/env/env.go | 4 ++++ server/utils/urls.go | 13 +++++++++++++ 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index 1629608..7097dcb 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "latest", + "@authorizerdev/authorizer-react": "^0.17.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", @@ -24,9 +24,9 @@ } }, "node_modules/@authorizerdev/authorizer-js": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.6.0.tgz", - "integrity": "sha512-WbqeUmhQwLNlvk4ZYTptlbAIINh7aZPyTCVA/B0FE3EoPtx1tNOtkPtJOycrn0H0HyueeXQnBSCDxkvPAP65Bw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.10.0.tgz", + "integrity": "sha512-REM8FLD/Ej9gzA2zDGDAke6QFss33ubePlTDmLDmIYUuQmpHFlO5mCCS6nVsKkN7F/Bcwkmp+eUNQjkdGCaKLg==", "dependencies": { "node-fetch": "^2.6.1" }, @@ -35,11 +35,11 @@ } }, "node_modules/@authorizerdev/authorizer-react": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.11.0.tgz", - "integrity": "sha512-VzSZvEB/t6N2ESn4O8c/+2hPUO7L4Iux8IBzXKrobKkoqRyb+u5TPZn0UWCOaoxIdiiZY+1Yq2A/H6q9LAqLGw==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.17.0.tgz", + "integrity": "sha512-7WcNCU7hDFkVfFb8LcJXFwWiLYd8aY78z1AbNPxCa2Cw5G85PaRkzjKybP6h01ITVOHO6M03lLwPj8p6Sr6fEg==", "dependencies": { - "@authorizerdev/authorizer-js": "^0.6.0", + "@authorizerdev/authorizer-js": "^0.10.0", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" @@ -829,19 +829,19 @@ }, "dependencies": { "@authorizerdev/authorizer-js": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.6.0.tgz", - "integrity": "sha512-WbqeUmhQwLNlvk4ZYTptlbAIINh7aZPyTCVA/B0FE3EoPtx1tNOtkPtJOycrn0H0HyueeXQnBSCDxkvPAP65Bw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.10.0.tgz", + "integrity": "sha512-REM8FLD/Ej9gzA2zDGDAke6QFss33ubePlTDmLDmIYUuQmpHFlO5mCCS6nVsKkN7F/Bcwkmp+eUNQjkdGCaKLg==", "requires": { "node-fetch": "^2.6.1" } }, "@authorizerdev/authorizer-react": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.11.0.tgz", - "integrity": "sha512-VzSZvEB/t6N2ESn4O8c/+2hPUO7L4Iux8IBzXKrobKkoqRyb+u5TPZn0UWCOaoxIdiiZY+1Yq2A/H6q9LAqLGw==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.17.0.tgz", + "integrity": "sha512-7WcNCU7hDFkVfFb8LcJXFwWiLYd8aY78z1AbNPxCa2Cw5G85PaRkzjKybP6h01ITVOHO6M03lLwPj8p6Sr6fEg==", "requires": { - "@authorizerdev/authorizer-js": "^0.6.0", + "@authorizerdev/authorizer-js": "^0.10.0", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" diff --git a/app/package.json b/app/package.json index cd974d6..8a3b954 100644 --- a/app/package.json +++ b/app/package.json @@ -11,7 +11,7 @@ "author": "Lakhan Samani", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "latest", + "@authorizerdev/authorizer-react": "^0.17.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 05e2da7..001e7a9 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -10,6 +10,9 @@ const queryClient = createClient({ fetchOptions: () => { return { credentials: 'include', + headers: { + 'x-authorizer-url': window.location.origin, + }, }; }, requestPolicy: 'network-only', diff --git a/server/constants/env.go b/server/constants/env.go index d9af456..66b466a 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -16,7 +16,6 @@ const ( // EnvKeyEnvPath key for cli arg variable ENV_PATH EnvKeyEnvPath = "ENV_PATH" // EnvKeyAuthorizerURL key for env variable AUTHORIZER_URL - // TODO: remove support AUTHORIZER_URL env EnvKeyAuthorizerURL = "AUTHORIZER_URL" // EnvKeyPort key for env variable PORT EnvKeyPort = "PORT" diff --git a/server/env/env.go b/server/env/env.go index 2861d91..7202d9b 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -113,6 +113,10 @@ func InitAllEnv() error { envData.StringEnv[constants.EnvKeyAppURL] = os.Getenv(constants.EnvKeyAppURL) } + if envData.StringEnv[constants.EnvKeyAuthorizerURL] == "" { + envData.StringEnv[constants.EnvKeyAuthorizerURL] = os.Getenv(constants.EnvKeyAuthorizerURL) + } + if envData.StringEnv[constants.EnvKeyPort] == "" { envData.StringEnv[constants.EnvKeyPort] = os.Getenv(constants.EnvKeyPort) if envData.StringEnv[constants.EnvKeyPort] == "" { diff --git a/server/utils/urls.go b/server/utils/urls.go index 390cfbd..f97582b 100644 --- a/server/utils/urls.go +++ b/server/utils/urls.go @@ -10,7 +10,20 @@ import ( ) // GetHost returns hostname from request context +// if X-Authorizer-URL header is set it is given highest priority +// if EnvKeyAuthorizerURL is set it is given second highest priority. +// if above 2 are not set the requesting host name is used func GetHost(c *gin.Context) string { + authorizerURL := c.Request.Header.Get("X-Authorizer-URL") + if authorizerURL != "" { + return authorizerURL + } + + authorizerURL = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + if authorizerURL != "" { + return authorizerURL + } + scheme := c.Request.Header.Get("X-Forwarded-Proto") if scheme != "https" { scheme = "http" From d5f1c5a5eb7fa2a41fcdcf572961bc98bb675b05 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sat, 2 Apr 2022 17:34:50 +0530 Subject: [PATCH 17/18] Resolves #156 --- server/db/models/verification_requests.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/db/models/verification_requests.go b/server/db/models/verification_requests.go index a1b30ad..cbb0322 100644 --- a/server/db/models/verification_requests.go +++ b/server/db/models/verification_requests.go @@ -7,11 +7,11 @@ type VerificationRequest struct { Key string `json:"_key,omitempty" bson:"_key"` // for arangodb ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"` Token string `gorm:"type:text" json:"token" bson:"token"` - Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"` + Identifier string `gorm:"uniqueIndex:idx_email_identifier;type:varchar(64)" json:"identifier" bson:"identifier"` ExpiresAt int64 `json:"expires_at" bson:"expires_at"` CreatedAt int64 `json:"created_at" bson:"created_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at"` - Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"` + Email string `gorm:"uniqueIndex:idx_email_identifier;type:varchar(256)" json:"email" bson:"email"` Nonce string `gorm:"type:text" json:"nonce" bson:"nonce"` RedirectURI string `gorm:"type:text" json:"redirect_uri" bson:"redirect_uri"` } From 75e44ff6986ed92668599548b0f25919064fea30 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sun, 10 Apr 2022 14:43:19 +0530 Subject: [PATCH 18/18] fix: cors error for x-authorizer-url --- server/middlewares/cors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/middlewares/cors.go b/server/middlewares/cors.go index 0b85a62..ca06721 100644 --- a/server/middlewares/cors.go +++ b/server/middlewares/cors.go @@ -15,7 +15,7 @@ func CORSMiddleware() gin.HandlerFunc { } c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-authorizer-url") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") if c.Request.Method == "OPTIONS" {