From c20c6832b5cfed706d564d0c2bc995db75564a9e Mon Sep 17 00:00:00 2001 From: Yash Mehrotra Date: Wed, 2 Aug 2023 17:47:46 +0530 Subject: [PATCH 1/5] feat: support for clerk auth --- auth/clerk_client.go | 170 +++++++++++++++++++++++++++ auth/{client.go => kratos_client.go} | 0 cmd/root.go | 7 +- cmd/server.go | 51 +++++--- db/people.go | 5 + go.mod | 10 ++ go.sum | 16 +++ 7 files changed, 242 insertions(+), 17 deletions(-) create mode 100644 auth/clerk_client.go rename auth/{client.go => kratos_client.go} (100%) diff --git a/auth/clerk_client.go b/auth/clerk_client.go new file mode 100644 index 000000000..03ce9a234 --- /dev/null +++ b/auth/clerk_client.go @@ -0,0 +1,170 @@ +package auth + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/clerkinc/clerk-sdk-go/clerk" + "github.com/flanksource/commons/logger" + "github.com/flanksource/incident-commander/api" + "github.com/flanksource/incident-commander/db" + "github.com/golang-jwt/jwt/v4" + "github.com/labstack/echo/v4" + "github.com/patrickmn/go-cache" +) + +type ClerkHandler struct { + client clerk.Client + dbJwtSecret string + tokenCache *cache.Cache + userCache *cache.Cache +} + +func NewClerkHandler(secretKey, dbJwtSecret string) (*ClerkHandler, error) { + client, err := clerk.NewClient(secretKey) + if err != nil { + return nil, err + } + + // TODO: Remove this block, debug use case only + users, err := client.Users().ListAll(clerk.ListAllUsersParams{}) + if err != nil { + logger.Errorf("ERR GETTING USERS %v", err) + } + for _, u := range users { + logger.Infof(u.ID) + } + ///// + return &ClerkHandler{ + client: client, + dbJwtSecret: dbJwtSecret, + tokenCache: cache.New(3*24*time.Hour, 12*time.Hour), + userCache: cache.New(3*24*time.Hour, 12*time.Hour), + }, nil +} + +func (h ClerkHandler) Session(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if canSkipAuth(c) { + return next(c) + } + + // Extract session token from Authorization header + sessionToken := c.Request().Header.Get(echo.HeaderAuthorization) + sessionToken = strings.TrimPrefix(sessionToken, "Bearer ") + + var ( + user *clerk.User + err error + sessID string + ) + user, sessID, err = h.getUser(sessionToken) + if err != nil { + logger.Errorf("Error fetching user from clerk: %v", err) + return c.String(http.StatusUnauthorized, "Unauthorized") + } + + ctx := c.(*api.Context) + if user.ExternalID == nil { + user, err = h.createDBUser(ctx, user) + if err != nil { + logger.Errorf("Error creating user in database from clerk: %v", err) + return c.String(http.StatusUnauthorized, "Unauthorized") + } + // Clear user from cache so that new metadata is used + h.userCache.Delete(sessionToken) + } + + token, err := h.getDBToken(sessID, *user.ExternalID) + if err != nil { + logger.Errorf("Error generating JWT Token: %v", err) + } + + c.Request().Header.Add(echo.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) + c.Request().Header.Add(UserIDHeaderKey, *user.ExternalID) + return next(c) + } +} + +func (h *ClerkHandler) generateDBToken(id string) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "role": DefaultPostgrestRole, + "id": id, + }) + return token.SignedString([]byte(h.dbJwtSecret)) +} + +func (h *ClerkHandler) getDBToken(sessionID, userID string) (string, error) { + cacheKey := sessionID + userID + if token, exists := h.tokenCache.Get(cacheKey); exists { + return token.(string), nil + } + // Adding Authorization Token for PostgREST + token, err := h.generateDBToken(userID) + if err != nil { + return "", err + } + h.tokenCache.SetDefault(cacheKey, token) + return token, nil +} + +func (h *ClerkHandler) getUser(sessionToken string) (*clerk.User, string, error) { + cacheKey := sessionToken + if user, exists := h.userCache.Get(cacheKey); exists { + return user.(*clerk.User), "", nil + } + + sessClaims, err := h.client.VerifyToken(sessionToken) + if err != nil { + return nil, "", err + } + + user, err := h.client.Users().Read(sessClaims.Claims.Subject) + if err != nil { + return nil, "", err + } + h.userCache.SetDefault(cacheKey, user) + return user, sessClaims.SessionID, nil +} + +func (h *ClerkHandler) createDBUser(ctx *api.Context, user *clerk.User) (*clerk.User, error) { + if user.ExternalID != nil { + return user, nil + } + if user.PrimaryEmailAddressID == nil { + return nil, fmt.Errorf("clerk.user[%s] has no primary email", user.ID) + } + + var email string + for _, addr := range user.EmailAddresses { + if addr.ID == *user.PrimaryEmailAddressID { + email = addr.EmailAddress + break + } + } + + var name []string + if user.FirstName != nil { + name = append(name, *user.FirstName) + } + if user.LastName != nil { + name = append(name, *user.LastName) + } + person := api.Person{ + Name: strings.Join(name, " "), + Email: email, + } + + dbUser, err := db.CreateUser(ctx, person) + if err != nil { + return nil, err + } + + id := dbUser.ID.String() + userUpdateParams := clerk.UpdateUser{ + ExternalID: &id, + } + return h.client.Users().Update(user.ID, &userUpdateParams) +} diff --git a/auth/client.go b/auth/kratos_client.go similarity index 100% rename from auth/client.go rename to auth/kratos_client.go diff --git a/cmd/root.go b/cmd/root.go index 3e9942675..7257690f8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -38,8 +38,8 @@ var Root = &cobra.Command{ var dev bool var httpPort, metricsPort, devGuiPort int var publicEndpoint = "http://localhost:8080" -var configDb, kratosAPI, kratosAdminAPI, postgrestURI string -var enableAuth, disablePostgrest bool +var configDb, authMode, kratosAPI, kratosAdminAPI, postgrestURI, clerkSecret string +var disablePostgrest bool func ServerFlags(flags *pflag.FlagSet) { flags.IntVar(&httpPort, "httpPort", 8080, "Port to expose a health dashboard") @@ -52,8 +52,9 @@ func ServerFlags(flags *pflag.FlagSet) { flags.StringVar(&configDb, "config-db", "http://config-db:8080", "Config DB URL") flags.StringVar(&kratosAPI, "kratos-api", "http://kratos-public:80", "Kratos API service") flags.StringVar(&kratosAdminAPI, "kratos-admin", "http://kratos-admin:80", "Kratos Admin API service") + flags.StringVar(&clerkSecret, "clerk-secret", "", "Clerk Secret Key") flags.StringVar(&postgrestURI, "postgrest-uri", "http://localhost:3000", "URL for the PostgREST instance to use. If localhost is supplied, a PostgREST instance will be started") - flags.BoolVar(&enableAuth, "enable-auth", false, "Enable authentication via Kratos") + flags.StringVar(&authMode, "auth", "", "Enable authentication via Kratos or Clerk. Valid values are [kratos, clerk]") flags.DurationVar(&rules.Period, "rules-period", 5*time.Minute, "Period to run the rules") flags.BoolVar(&disablePostgrest, "disable-postgrest", false, "Disable PostgREST. Deprecated (Use --postgrest-uri '' to disable PostgREST)") flags.StringVar(&mail.FromAddress, "email-from-address", "no-reply@flanksource.com", "Email address of the sender") diff --git a/cmd/server.go b/cmd/server.go index aff76bf81..459063d78 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -7,6 +7,7 @@ import ( "net/url" "strings" + "github.com/flanksource/commons/collections" "github.com/flanksource/commons/logger" "github.com/flanksource/duty/schema/openapi" "github.com/flanksource/kopper" @@ -58,22 +59,46 @@ func createHTTPServer(gormDB *gorm.DB) *echo.Echo { e.Use(echoprometheus.NewMiddleware("mission_control")) e.GET("/metrics", echoprometheus.NewHandler()) - kratosHandler := auth.NewKratosHandler(kratosAPI, kratosAdminAPI, db.PostgRESTJWTSecret) - if enableAuth { - adminUserID, err := kratosHandler.CreateAdminUser(context.Background()) - if err != nil { - logger.Fatalf("Failed to created admin user: %v", err) + if authMode != "" { + if !collections.Contains([]string{"kratos", "clerk"}, authMode) { + logger.Fatalf("Invalid auth provider: %s", authMode) } - middleware, err := kratosHandler.KratosMiddleware() - if err != nil { - logger.Fatalf("Failed to initialize kratos middleware: %v", err) + var ( + adminUserID string + err error + ) + if authMode == "kratos" { + kratosHandler := auth.NewKratosHandler(kratosAPI, kratosAdminAPI, db.PostgRESTJWTSecret) + adminUserID, err = kratosHandler.CreateAdminUser(context.Background()) + if err != nil { + logger.Fatalf("Failed to created admin user: %v", err) + } + + middleware, err := kratosHandler.KratosMiddleware() + if err != nil { + logger.Fatalf("Failed to initialize kratos middleware: %v", err) + } + e.Use(middleware.Session) + e.POST("/auth/invite_user", kratosHandler.InviteUser, rbac.Authorization(rbac.ObjectAuth, rbac.ActionWrite)) + } + + if authMode == "clerk" { + clerkHandler, err := auth.NewClerkHandler(clerkSecret, db.PostgRESTJWTSecret) + if err != nil { + logger.Fatalf("Failed to initialize clerk client: %v", err) + } + e.Use(clerkHandler.Session) } - e.Use(middleware.Session) + if adminUserID == "" { + logger.Fatalf("Admin user cannot be empty") + } // Initiate RBAC - if err := rbac.Init(adminUserID); err != nil { - logger.Fatalf("Failed to initialize rbac: %v", err) + if adminUserID != "" { + if err := rbac.Init(adminUserID); err != nil { + logger.Fatalf("Failed to initialize rbac: %v", err) + } } } @@ -94,8 +119,6 @@ func createHTTPServer(gormDB *gorm.DB) *echo.Echo { return c.JSON(http.StatusOK, api.HTTPSuccess{Message: "ok"}) }) - e.POST("/auth/invite_user", kratosHandler.InviteUser, rbac.Authorization(rbac.ObjectAuth, rbac.ActionWrite)) - e.GET("/snapshot/topology/:id", snapshot.Topology) e.GET("/snapshot/incident/:id", snapshot.Incident) e.GET("/snapshot/config/:id", snapshot.Config) @@ -167,7 +190,7 @@ var Serve = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { // PostgREST needs to know how it is exposed to create the correct links db.HttpEndpoint = publicEndpoint + "/db" - if !enableAuth { + if authMode != "" { db.PostgresDBAnonRole = "postgrest_api" } diff --git a/db/people.go b/db/people.go index d35a583a8..5869d180c 100644 --- a/db/people.go +++ b/db/people.go @@ -22,3 +22,8 @@ func UpdateUserProperties(ctx *api.Context, userID string, newProps api.PersonPr func UpdateIdentityState(ctx *api.Context, id, state string) error { return ctx.DB().Table("identities").Where("id = ?", id).Update("state", state).Error } + +func CreateUser(ctx *api.Context, user api.Person) (api.Person, error) { + err := ctx.DB().Table("people").Create(&user).Error + return user, err +} diff --git a/go.mod b/go.mod index 4cfc4c42a..c26f41cbd 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/andygrunwald/go-jira v1.16.0 github.com/casbin/casbin/v2 v2.73.0 github.com/casbin/gorm-adapter/v3 v3.18.0 + github.com/clerkinc/clerk-sdk-go v1.47.0 github.com/containrrr/shoutrrr v0.7.1 github.com/fergusstrange/embedded-postgres v1.23.0 github.com/flanksource/commons v1.10.2 @@ -55,8 +56,17 @@ require ( github.com/flanksource/is-healthy v0.0.0-20230713150444-ad2a5ef4bb37 // indirect github.com/flanksource/mapstructure v1.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect +<<<<<<< HEAD github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/sqlite v1.9.0 // indirect +======= + github.com/glebarez/go-sqlite v1.20.3 // indirect + github.com/glebarez/sqlite v1.7.0 // indirect + github.com/go-git/gcfg v1.5.0 // indirect + github.com/go-git/go-billy/v5 v5.4.1 // indirect + github.com/go-git/go-git/v5 v5.6.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect +>>>>>>> 6144671 (feat: support for clerk auth) github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/inflect v0.19.0 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect diff --git a/go.sum b/go.sum index 21a933200..3b5ce23c6 100644 --- a/go.sum +++ b/go.sum @@ -678,6 +678,15 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/brianvoe/gofakeit/v6 v6.19.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/casbin/casbin/v2 v2.73.0 h1:Qgy70fd90wXrDvSLBAFrDBNYv34lCqppK24vF0OHv/M= github.com/casbin/casbin/v2 v2.73.0/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk= @@ -700,6 +709,9 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/cjlapao/common-go v0.0.39 h1:bAAUrj2B9v0kMzbAOhzjSmiyDy+rd56r2sy7oEiQLlA= github.com/cjlapao/common-go v0.0.39/go.mod h1:M3dzazLjTjEtZJbbxoA5ZDiGCiHmpwqW9l4UWaddwOA= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/clerkinc/clerk-sdk-go v1.47.0 h1:vUDLuUGajNxx5s6gO5VRhUyhJYrL8KUw8TqQPXKHWC0= +github.com/clerkinc/clerk-sdk-go v1.47.0/go.mod h1:4ARydv3sHP9zv+8w4mGpUHdesIFTjGy9dTVW+9sb0+8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -798,6 +810,9 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -1440,6 +1455,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= From 354ca320867a9a0a48b1ee16fc53bab7a03002ef Mon Sep 17 00:00:00 2001 From: Yash Mehrotra Date: Wed, 2 Aug 2023 18:15:03 +0530 Subject: [PATCH 2/5] chore: use auth modes for enabling provider --- cmd/server.go | 24 +++++++++--------------- go.mod | 8 -------- go.sum | 11 +---------- rbac/init.go | 9 ++++----- 4 files changed, 14 insertions(+), 38 deletions(-) diff --git a/cmd/server.go b/cmd/server.go index 459063d78..a2b0fc0e9 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -7,7 +7,6 @@ import ( "net/url" "strings" - "github.com/flanksource/commons/collections" "github.com/flanksource/commons/logger" "github.com/flanksource/duty/schema/openapi" "github.com/flanksource/kopper" @@ -60,15 +59,13 @@ func createHTTPServer(gormDB *gorm.DB) *echo.Echo { e.GET("/metrics", echoprometheus.NewHandler()) if authMode != "" { - if !collections.Contains([]string{"kratos", "clerk"}, authMode) { - logger.Fatalf("Invalid auth provider: %s", authMode) - } - var ( adminUserID string err error ) - if authMode == "kratos" { + + switch authMode { + case "kratos": kratosHandler := auth.NewKratosHandler(kratosAPI, kratosAdminAPI, db.PostgRESTJWTSecret) adminUserID, err = kratosHandler.CreateAdminUser(context.Background()) if err != nil { @@ -81,24 +78,21 @@ func createHTTPServer(gormDB *gorm.DB) *echo.Echo { } e.Use(middleware.Session) e.POST("/auth/invite_user", kratosHandler.InviteUser, rbac.Authorization(rbac.ObjectAuth, rbac.ActionWrite)) - } - if authMode == "clerk" { + case "clerk": clerkHandler, err := auth.NewClerkHandler(clerkSecret, db.PostgRESTJWTSecret) if err != nil { logger.Fatalf("Failed to initialize clerk client: %v", err) } e.Use(clerkHandler.Session) - } - if adminUserID == "" { - logger.Fatalf("Admin user cannot be empty") + default: + logger.Fatalf("Invalid auth provider: %s", authMode) } + // Initiate RBAC - if adminUserID != "" { - if err := rbac.Init(adminUserID); err != nil { - logger.Fatalf("Failed to initialize rbac: %v", err) - } + if err := rbac.Init(adminUserID); err != nil { + logger.Fatalf("Failed to initialize rbac: %v", err) } } diff --git a/go.mod b/go.mod index c26f41cbd..751a2c522 100644 --- a/go.mod +++ b/go.mod @@ -56,17 +56,9 @@ require ( github.com/flanksource/is-healthy v0.0.0-20230713150444-ad2a5ef4bb37 // indirect github.com/flanksource/mapstructure v1.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect -<<<<<<< HEAD github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/sqlite v1.9.0 // indirect -======= - github.com/glebarez/go-sqlite v1.20.3 // indirect - github.com/glebarez/sqlite v1.7.0 // indirect - github.com/go-git/gcfg v1.5.0 // indirect - github.com/go-git/go-billy/v5 v5.4.1 // indirect - github.com/go-git/go-git/v5 v5.6.1 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect ->>>>>>> 6144671 (feat: support for clerk auth) github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/inflect v0.19.0 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect diff --git a/go.sum b/go.sum index 3b5ce23c6..440270f2f 100644 --- a/go.sum +++ b/go.sum @@ -678,15 +678,7 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/brianvoe/gofakeit/v6 v6.19.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= -github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/casbin/casbin/v2 v2.73.0 h1:Qgy70fd90wXrDvSLBAFrDBNYv34lCqppK24vF0OHv/M= github.com/casbin/casbin/v2 v2.73.0/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk= @@ -709,7 +701,6 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/cjlapao/common-go v0.0.39 h1:bAAUrj2B9v0kMzbAOhzjSmiyDy+rd56r2sy7oEiQLlA= github.com/cjlapao/common-go v0.0.39/go.mod h1:M3dzazLjTjEtZJbbxoA5ZDiGCiHmpwqW9l4UWaddwOA= -github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/clerkinc/clerk-sdk-go v1.47.0 h1:vUDLuUGajNxx5s6gO5VRhUyhJYrL8KUw8TqQPXKHWC0= github.com/clerkinc/clerk-sdk-go v1.47.0/go.mod h1:4ARydv3sHP9zv+8w4mGpUHdesIFTjGy9dTVW+9sb0+8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -810,7 +801,6 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -1460,6 +1450,7 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= diff --git a/rbac/init.go b/rbac/init.go index 550eb23ff..9a8213a44 100644 --- a/rbac/init.go +++ b/rbac/init.go @@ -77,11 +77,10 @@ func Init(adminUserID string) error { return fmt.Errorf("error creating rbac enforcer: %v", err) } - if adminUserID == "" { - return fmt.Errorf("admin user cannot be empty") - } - if _, err := Enforcer.AddRoleForUser(adminUserID, RoleAdmin); err != nil { - return fmt.Errorf("error adding role for admin user: %v", err) + if adminUserID != "" { + if _, err := Enforcer.AddRoleForUser(adminUserID, RoleAdmin); err != nil { + return fmt.Errorf("error adding role for admin user: %v", err) + } } // Hierarchial policies From 9d2f824c7d376e593d319969f09798a199136227 Mon Sep 17 00:00:00 2001 From: Yash Mehrotra Date: Thu, 3 Aug 2023 11:08:34 +0530 Subject: [PATCH 3/5] chore: use session id for cache instead of token --- auth/clerk_client.go | 23 ++++------- auth/kratos_client.go | 83 ++++++++++++++++++++++++++++++++++++++++ auth/users.go | 88 ------------------------------------------- db/people.go | 9 ++++- 4 files changed, 98 insertions(+), 105 deletions(-) delete mode 100644 auth/users.go diff --git a/auth/clerk_client.go b/auth/clerk_client.go index 03ce9a234..f47b8cfec 100644 --- a/auth/clerk_client.go +++ b/auth/clerk_client.go @@ -28,15 +28,6 @@ func NewClerkHandler(secretKey, dbJwtSecret string) (*ClerkHandler, error) { return nil, err } - // TODO: Remove this block, debug use case only - users, err := client.Users().ListAll(clerk.ListAllUsersParams{}) - if err != nil { - logger.Errorf("ERR GETTING USERS %v", err) - } - for _, u := range users { - logger.Infof(u.ID) - } - ///// return &ClerkHandler{ client: client, dbJwtSecret: dbJwtSecret, @@ -74,7 +65,7 @@ func (h ClerkHandler) Session(next echo.HandlerFunc) echo.HandlerFunc { return c.String(http.StatusUnauthorized, "Unauthorized") } // Clear user from cache so that new metadata is used - h.userCache.Delete(sessionToken) + h.userCache.Delete(sessID) } token, err := h.getDBToken(sessID, *user.ExternalID) @@ -111,16 +102,16 @@ func (h *ClerkHandler) getDBToken(sessionID, userID string) (string, error) { } func (h *ClerkHandler) getUser(sessionToken string) (*clerk.User, string, error) { - cacheKey := sessionToken - if user, exists := h.userCache.Get(cacheKey); exists { - return user.(*clerk.User), "", nil - } - sessClaims, err := h.client.VerifyToken(sessionToken) if err != nil { return nil, "", err } + cacheKey := sessClaims.SessionID + if user, exists := h.userCache.Get(cacheKey); exists { + return user.(*clerk.User), "", nil + } + user, err := h.client.Users().Read(sessClaims.Claims.Subject) if err != nil { return nil, "", err @@ -157,7 +148,7 @@ func (h *ClerkHandler) createDBUser(ctx *api.Context, user *clerk.User) (*clerk. Email: email, } - dbUser, err := db.CreateUser(ctx, person) + dbUser, err := db.GetOrCreateUser(ctx, person) if err != nil { return nil, err } diff --git a/auth/kratos_client.go b/auth/kratos_client.go index c58507071..ea91a2ac0 100644 --- a/auth/kratos_client.go +++ b/auth/kratos_client.go @@ -1,6 +1,10 @@ package auth import ( + "context" + "os" + + "github.com/flanksource/incident-commander/db" client "github.com/ory/client-go" ) @@ -31,3 +35,82 @@ func newKratosClient(apiURL string) *client.APIClient { configuration.Servers = []client.ServerConfiguration{{URL: apiURL}} return client.NewAPIClient(configuration) } + +const ( + AdminName = "Admin" + AdminEmail = "admin@local" + DefaultAdminPassword = "admin" +) + +func (k *KratosHandler) createUser(ctx context.Context, firstName, lastName, email string) (*client.Identity, error) { + adminCreateIdentityBody := *client.NewCreateIdentityBody( + "default", + map[string]any{ + "email": email, + "name": map[string]string{ + "first": firstName, + "last": lastName, + }, + }, + ) + + createdIdentity, _, err := k.adminClient.IdentityApi.CreateIdentity(ctx).CreateIdentityBody(adminCreateIdentityBody).Execute() + return createdIdentity, err +} + +func (k *KratosHandler) createRecoveryLink(ctx context.Context, id string) (string, error) { + adminCreateSelfServiceRecoveryLinkBody := client.NewCreateRecoveryLinkForIdentityBody(id) + resp, _, err := k.adminClient.IdentityApi.CreateRecoveryLinkForIdentity(ctx).CreateRecoveryLinkForIdentityBody(*adminCreateSelfServiceRecoveryLinkBody).Execute() + if err != nil { + return "", err + } + return resp.GetRecoveryLink(), nil +} + +func (k *KratosHandler) createAdminIdentity(ctx context.Context) (string, error) { + adminPassword := os.Getenv("ADMIN_PASSWORD") + if adminPassword == "" { + adminPassword = DefaultAdminPassword + } + + config := *client.NewIdentityWithCredentialsPasswordConfig() + config.SetPassword(adminPassword) + + password := *client.NewIdentityWithCredentialsPassword() + password.SetConfig(config) + + creds := *client.NewIdentityWithCredentials() + creds.SetPassword(password) + + body := *client.NewCreateIdentityBody( + "default", + map[string]any{ + "email": AdminEmail, + "name": map[string]string{ + "first": AdminName, + }, + }, + ) + body.SetCredentials(creds) + + createdIdentity, _, err := k.adminClient.IdentityApi.CreateIdentity(ctx).CreateIdentityBody(body).Execute() + if err != nil { + return "", err + } + + return createdIdentity.Id, nil +} + +func (k *KratosHandler) CreateAdminUser(ctx context.Context) (string, error) { + var id string + tx := db.Gorm.Raw(`SELECT id FROM identities WHERE traits->>'email' = ?`, AdminEmail).Scan(&id) + if tx.Error != nil { + return "", tx.Error + } + + if tx.RowsAffected == 0 { + return k.createAdminIdentity(ctx) + } + + return id, nil +} diff --git a/auth/users.go b/auth/users.go deleted file mode 100644 index 7e503e6d1..000000000 --- a/auth/users.go +++ /dev/null @@ -1,88 +0,0 @@ -package auth - -import ( - "context" - "os" - - "github.com/flanksource/incident-commander/db" - client "github.com/ory/client-go" -) - -const ( - AdminName = "Admin" - AdminEmail = "admin@local" - DefaultAdminPassword = "admin" -) - -func (k *KratosHandler) createUser(ctx context.Context, firstName, lastName, email string) (*client.Identity, error) { - adminCreateIdentityBody := *client.NewCreateIdentityBody( - "default", - map[string]any{ - "email": email, - "name": map[string]string{ - "first": firstName, - "last": lastName, - }, - }, - ) - - createdIdentity, _, err := k.adminClient.IdentityApi.CreateIdentity(ctx).CreateIdentityBody(adminCreateIdentityBody).Execute() - return createdIdentity, err -} - -func (k *KratosHandler) createRecoveryLink(ctx context.Context, id string) (string, error) { - adminCreateSelfServiceRecoveryLinkBody := client.NewCreateRecoveryLinkForIdentityBody(id) - resp, _, err := k.adminClient.IdentityApi.CreateRecoveryLinkForIdentity(ctx).CreateRecoveryLinkForIdentityBody(*adminCreateSelfServiceRecoveryLinkBody).Execute() - if err != nil { - return "", err - } - return resp.GetRecoveryLink(), nil -} - -func (k *KratosHandler) createAdminIdentity(ctx context.Context) (string, error) { - adminPassword := os.Getenv("ADMIN_PASSWORD") - if adminPassword == "" { - adminPassword = DefaultAdminPassword - } - - config := *client.NewIdentityWithCredentialsPasswordConfig() - config.SetPassword(adminPassword) - - password := *client.NewIdentityWithCredentialsPassword() - password.SetConfig(config) - - creds := *client.NewIdentityWithCredentials() - creds.SetPassword(password) - - body := *client.NewCreateIdentityBody( - "default", - map[string]any{ - "email": AdminEmail, - "name": map[string]string{ - "first": AdminName, - }, - }, - ) - body.SetCredentials(creds) - - createdIdentity, _, err := k.adminClient.IdentityApi.CreateIdentity(ctx).CreateIdentityBody(body).Execute() - if err != nil { - return "", err - } - - return createdIdentity.Id, nil -} - -func (k *KratosHandler) CreateAdminUser(ctx context.Context) (string, error) { - var id string - tx := db.Gorm.Raw(`SELECT id FROM identities WHERE traits->>'email' = ?`, AdminEmail).Scan(&id) - if tx.Error != nil { - return "", tx.Error - } - - if tx.RowsAffected == 0 { - return k.createAdminIdentity(ctx) - } - - return id, nil -} diff --git a/db/people.go b/db/people.go index 5869d180c..2f1d09f47 100644 --- a/db/people.go +++ b/db/people.go @@ -3,6 +3,7 @@ package db import ( "github.com/flanksource/incident-commander/api" "github.com/flanksource/incident-commander/utils" + "github.com/google/uuid" ) func UpdateUserProperties(ctx *api.Context, userID string, newProps api.PersonProperties) error { @@ -23,7 +24,13 @@ func UpdateIdentityState(ctx *api.Context, id, state string) error { return ctx.DB().Table("identities").Where("id = ?", id).Update("state", state).Error } -func CreateUser(ctx *api.Context, user api.Person) (api.Person, error) { +func GetOrCreateUser(ctx *api.Context, user api.Person) (api.Person, error) { + if err := ctx.DB().Table("people").Where("email = ?", user.Email).Find(&user).Error; err != nil { + return api.Person{}, err + } + if user.ID != uuid.Nil { + return user, nil + } err := ctx.DB().Table("people").Create(&user).Error return user, err } From c4bb71f9ffb3512fbe8c4c8ca5fa532483edbc61 Mon Sep 17 00:00:00 2001 From: Yash Mehrotra Date: Fri, 4 Aug 2023 10:49:51 +0530 Subject: [PATCH 4/5] chore: error handling for jwt failure --- auth/clerk_client.go | 10 +++------- auth/middleware.go | 1 + 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/auth/clerk_client.go b/auth/clerk_client.go index f47b8cfec..d3cc48963 100644 --- a/auth/clerk_client.go +++ b/auth/clerk_client.go @@ -46,12 +46,7 @@ func (h ClerkHandler) Session(next echo.HandlerFunc) echo.HandlerFunc { sessionToken := c.Request().Header.Get(echo.HeaderAuthorization) sessionToken = strings.TrimPrefix(sessionToken, "Bearer ") - var ( - user *clerk.User - err error - sessID string - ) - user, sessID, err = h.getUser(sessionToken) + user, sessID, err := h.getUser(sessionToken) if err != nil { logger.Errorf("Error fetching user from clerk: %v", err) return c.String(http.StatusUnauthorized, "Unauthorized") @@ -71,6 +66,7 @@ func (h ClerkHandler) Session(next echo.HandlerFunc) echo.HandlerFunc { token, err := h.getDBToken(sessID, *user.ExternalID) if err != nil { logger.Errorf("Error generating JWT Token: %v", err) + return c.String(http.StatusUnauthorized, "Unauthorized") } c.Request().Header.Add(echo.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) @@ -109,7 +105,7 @@ func (h *ClerkHandler) getUser(sessionToken string) (*clerk.User, string, error) cacheKey := sessClaims.SessionID if user, exists := h.userCache.Get(cacheKey); exists { - return user.(*clerk.User), "", nil + return user.(*clerk.User), sessClaims.SessionID, nil } user, err := h.client.Users().Read(sessClaims.Claims.Subject) diff --git a/auth/middleware.go b/auth/middleware.go index f84ee4c4f..0406ccb1e 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -67,6 +67,7 @@ func (k *kratosMiddleware) Session(next echo.HandlerFunc) echo.HandlerFunc { token, err := k.getDBToken(session.Id, session.Identity.GetId()) if err != nil { logger.Errorf("Error generating JWT Token: %v", err) + return c.String(http.StatusUnauthorized, "Unauthorized") } c.Request().Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) c.Request().Header.Add(UserIDHeaderKey, session.Identity.GetId()) From 6784b8b34e490a8110aadd89dbfd27281a764c14 Mon Sep 17 00:00:00 2001 From: Yash Mehrotra Date: Fri, 4 Aug 2023 20:28:06 +0530 Subject: [PATCH 5/5] chore: add support for cookies in auth middleware --- auth/clerk_client.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/auth/clerk_client.go b/auth/clerk_client.go index d3cc48963..6ae0374b5 100644 --- a/auth/clerk_client.go +++ b/auth/clerk_client.go @@ -15,6 +15,10 @@ import ( "github.com/patrickmn/go-cache" ) +const ( + clerkSessionCookie = "_session" +) + type ClerkHandler struct { client clerk.Client dbJwtSecret string @@ -45,6 +49,15 @@ func (h ClerkHandler) Session(next echo.HandlerFunc) echo.HandlerFunc { // Extract session token from Authorization header sessionToken := c.Request().Header.Get(echo.HeaderAuthorization) sessionToken = strings.TrimPrefix(sessionToken, "Bearer ") + if sessionToken == "" { + // Check for `_session` cookie + sessionTokenCookie, err := c.Request().Cookie(clerkSessionCookie) + if err != nil { + // Cookie not found + return c.String(http.StatusUnauthorized, "Unauthorized") + } + sessionToken = sessionTokenCookie.Value + } user, sessID, err := h.getUser(sessionToken) if err != nil {