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) + }) + }) +}