From 3bb90acc9e4cad11f2d86fddf7db4de5fcfe4ccf Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 8 Mar 2022 18:49:42 +0530 Subject: [PATCH] feat: add revoke mutation + handler --- server/graph/generated/generated.go | 109 ++++++++++++++++++++++++++++ server/graph/model/models_gen.go | 4 + server/graph/schema.graphqls | 5 ++ server/graph/schema.resolvers.go | 10 ++- server/handlers/logout.go | 1 + server/handlers/revoke.go | 50 +++++++++++++ server/handlers/token.go | 1 + server/resolvers/revoke.go | 16 ++++ server/routes/routes.go | 1 + 9 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 server/handlers/revoke.go create mode 100644 server/resolvers/revoke.go diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index ff0175d..3327538 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -119,6 +119,7 @@ type ComplexityRoot struct { MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int ResetPassword func(childComplexity int, params model.ResetPasswordInput) int + Revoke func(childComplexity int, params model.OAuthRevokeInput) 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 @@ -200,6 +201,7 @@ type MutationResolver interface { ResendVerifyEmail(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, error) ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error) ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error) + Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error) AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error) @@ -713,6 +715,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.ResetPassword(childComplexity, args["params"].(model.ResetPasswordInput)), true + case "Mutation.revoke": + if e.complexity.Mutation.Revoke == nil { + break + } + + args, err := ec.field_Mutation_revoke_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.Revoke(childComplexity, args["params"].(model.OAuthRevokeInput)), true + case "Mutation.signup": if e.complexity.Mutation.Signup == nil { break @@ -1415,6 +1429,10 @@ input PaginatedInput { pagination: PaginationInput } +input OAuthRevokeInput { + refresh_token: String! +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -1425,6 +1443,7 @@ type Mutation { resend_verify_email(params: ResendVerifyEmailInput!): Response! forgot_password(params: ForgotPasswordInput!): Response! reset_password(params: ResetPasswordInput!): Response! + revoke(params: OAuthRevokeInput!): Response! # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! @@ -1602,6 +1621,21 @@ func (ec *executionContext) field_Mutation_reset_password_args(ctx context.Conte return args, nil } +func (ec *executionContext) field_Mutation_revoke_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.OAuthRevokeInput + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNOAuthRevokeInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐOAuthRevokeInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_signup_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3860,6 +3894,48 @@ func (ec *executionContext) _Mutation_reset_password(ctx context.Context, field return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_revoke(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_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().Revoke(rctx, args["params"].(model.OAuthRevokeInput)) + }) + 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__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -6939,6 +7015,29 @@ func (ec *executionContext) unmarshalInputMagicLinkLoginInput(ctx context.Contex return it, nil } +func (ec *executionContext) unmarshalInputOAuthRevokeInput(ctx context.Context, obj interface{}) (model.OAuthRevokeInput, error) { + var it model.OAuthRevokeInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "refresh_token": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("refresh_token")) + it.RefreshToken, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputPaginatedInput(ctx context.Context, obj interface{}) (model.PaginatedInput, error) { var it model.PaginatedInput asMap := map[string]interface{}{} @@ -8039,6 +8138,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "revoke": + out.Values[i] = ec._Mutation_revoke(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "_delete_user": out.Values[i] = ec._Mutation__delete_user(ctx, field) if out.Values[i] == graphql.Null { @@ -8822,6 +8926,11 @@ func (ec *executionContext) marshalNMeta2ᚖgithubᚗcomᚋauthorizerdevᚋautho return ec._Meta(ctx, sel, v) } +func (ec *executionContext) unmarshalNOAuthRevokeInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐOAuthRevokeInput(ctx context.Context, v interface{}) (model.OAuthRevokeInput, error) { + res, err := ec.unmarshalInputOAuthRevokeInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNPagination2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐPagination(ctx context.Context, sel ast.SelectionSet, v *model.Pagination) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 2825956..9f4cdd9 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -100,6 +100,10 @@ type Meta struct { IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` } +type OAuthRevokeInput struct { + RefreshToken string `json:"refresh_token"` +} + type PaginatedInput struct { Pagination *PaginationInput `json:"pagination"` } diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index b1549d2..cf76429 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -267,6 +267,10 @@ input PaginatedInput { pagination: PaginationInput } +input OAuthRevokeInput { + refresh_token: String! +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -277,6 +281,7 @@ type Mutation { resend_verify_email(params: ResendVerifyEmailInput!): Response! forgot_password(params: ForgotPasswordInput!): Response! reset_password(params: ResetPasswordInput!): Response! + revoke(params: OAuthRevokeInput!): Response! # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 79f718a..8830195 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -47,6 +47,10 @@ func (r *mutationResolver) ResetPassword(ctx context.Context, params model.Reset return resolvers.ResetPasswordResolver(ctx, params) } +func (r *mutationResolver) Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) { + return resolvers.RevokeResolver(ctx, params) +} + func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) { return resolvers.DeleteUserResolver(ctx, params) } @@ -105,5 +109,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/handlers/logout.go b/server/handlers/logout.go index 9ffc6cc..f3090aa 100644 --- a/server/handlers/logout.go +++ b/server/handlers/logout.go @@ -9,6 +9,7 @@ import ( "github.com/gin-gonic/gin" ) +// Handler to logout user func LogoutHandler() gin.HandlerFunc { return func(gc *gin.Context) { // get fingerprint hash diff --git a/server/handlers/revoke.go b/server/handlers/revoke.go new file mode 100644 index 0000000..6dc79db --- /dev/null +++ b/server/handlers/revoke.go @@ -0,0 +1,50 @@ +package handlers + +import ( + "net/http" + "strings" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/envstore" + "github.com/authorizerdev/authorizer/server/sessionstore" + "github.com/gin-gonic/gin" +) + +// Revoke handler to revoke refresh token +func RevokeHandler() gin.HandlerFunc { + return func(gc *gin.Context) { + var reqBody map[string]string + if err := gc.BindJSON(&reqBody); err != nil { + gc.JSON(http.StatusBadRequest, gin.H{ + "error": "error_binding_json", + "error_description": err.Error(), + }) + return + } + // get fingerprint hash + refreshToken := strings.TrimSpace(reqBody["refresh_token"]) + clientID := strings.TrimSpace(reqBody["client_id"]) + + if clientID == "" { + gc.JSON(http.StatusBadRequest, gin.H{ + "error": "client_id_required", + "error_description": "The client id is required", + }) + return + } + + if clientID != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) { + gc.JSON(http.StatusBadRequest, gin.H{ + "error": "invalid_client_id", + "error_description": "The client id is invalid", + }) + return + } + + sessionstore.RemoveState(refreshToken) + + gc.JSON(http.StatusOK, gin.H{ + "message": "Token revoked successfully", + }) + } +} diff --git a/server/handlers/token.go b/server/handlers/token.go index fdfbf9f..aa34045 100644 --- a/server/handlers/token.go +++ b/server/handlers/token.go @@ -15,6 +15,7 @@ import ( "github.com/gin-gonic/gin" ) +// TokenHandler to handle /oauth/token requests // grant type required func TokenHandler() gin.HandlerFunc { return func(gc *gin.Context) { diff --git a/server/resolvers/revoke.go b/server/resolvers/revoke.go new file mode 100644 index 0000000..1ab1cb9 --- /dev/null +++ b/server/resolvers/revoke.go @@ -0,0 +1,16 @@ +package resolvers + +import ( + "context" + + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/sessionstore" +) + +// RevokeResolver resolver to revoke refresh token +func RevokeResolver(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) { + sessionstore.RemoveState(params.RefreshToken) + return &model.Response{ + Message: "Token revoked", + }, nil +} diff --git a/server/routes/routes.go b/server/routes/routes.go index 6b99d3a..71e1926 100644 --- a/server/routes/routes.go +++ b/server/routes/routes.go @@ -27,6 +27,7 @@ func InitRouter() *gin.Engine { router.GET("/userinfo", handlers.UserInfoHandler()) router.GET("/logout", handlers.LogoutHandler()) router.POST("/oauth/token", handlers.TokenHandler()) + router.POST("/oauth/revoke", handlers.RevokeHandler()) router.LoadHTMLGlob("templates/*") // login page app related routes.