From 9beff44f093157be9ffffc3315147d304b8fbe63 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 24 Dec 2023 15:20:57 +0100 Subject: [PATCH] Implement Ory authentication --- api/auth.go | 28 +- api/routes.go | 7 +- api/user_auth.go | 23 +- config/cli.go | 1 + config/config.go | 3 + go.mod | 3 +- go.sum | 4 + internal/app/app.go | 14 +- ui/routes.go | 5 + .../components/ForgottenPasswordForm.svelte | 3 - ui/src/lib/components/Header.svelte | 29 +- ui/src/lib/components/KratosFlow.svelte | 72 +++++ ui/src/lib/components/KratosForm.svelte | 250 ++++++++++++++++++ ui/src/lib/components/KratosNode.svelte | 160 +++++++++++ ui/src/lib/components/LoginForm.svelte | 13 +- ui/src/lib/components/SignUpForm.svelte | 11 +- ui/src/lib/locales/en.json | 1 + ui/src/lib/locales/fr.json | 1 + ui/src/routes/forgotten-password/+page.svelte | 11 +- ui/src/routes/join/+page.svelte | 27 +- ui/src/routes/login/+page.svelte | 24 +- ui/src/routes/me/+page.svelte | 14 +- 22 files changed, 660 insertions(+), 44 deletions(-) create mode 100644 ui/src/lib/components/KratosFlow.svelte create mode 100644 ui/src/lib/components/KratosForm.svelte create mode 100644 ui/src/lib/components/KratosNode.svelte diff --git a/api/auth.go b/api/auth.go index dbb339b..1a2da03 100644 --- a/api/auth.go +++ b/api/auth.go @@ -30,6 +30,8 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + ory "github.com/ory/client-go" "git.happydns.org/happyDomain/actions" "git.happydns.org/happyDomain/config" @@ -139,7 +141,7 @@ func requireLogin(opts *config.Options, c *gin.Context, msg string) { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": msg}) } -func authMiddleware(opts *config.Options, optional bool) gin.HandlerFunc { +func authMiddleware(opts *config.Options, o *ory.APIClient, optional bool) gin.HandlerFunc { return func(c *gin.Context) { var token string @@ -150,6 +152,30 @@ func authMiddleware(opts *config.Options, optional bool) gin.HandlerFunc { token = flds[1] } + if o != nil { + cookies := c.Request.Header.Get("Cookie") + + session, _, err := o.FrontendAPI.ToSession(c.Request.Context()).Cookie(cookies).TokenizeAs("jwt_happydomain").Execute() + if !((err != nil && session == nil) || (err == nil && !*session.Active)) { + if session.Tokenized != nil { + token = *session.Tokenized + setCookie(opts, c, token) + } else if session.Identity != nil && len(session.Identity.VerifiableAddresses) >= 0 { + uid, err := uuid.Parse(session.Identity.Id) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to parse user UUID"}) + return + } + tmp := [16]byte(uid) + _, token, _ = completeAuth(opts, c, UserProfile{ + UserId: tmp[:], + Email: session.Identity.VerifiableAddresses[0].Value, + EmailVerified: session.Identity.VerifiableAddresses[0].Verified, + }) + } + } + } + // Stop here if there is no cookie if len(token) == 0 { if optional { diff --git a/api/routes.go b/api/routes.go index e99ebaf..0f51ad7 100644 --- a/api/routes.go +++ b/api/routes.go @@ -23,6 +23,7 @@ package api import ( "github.com/gin-gonic/gin" + ory "github.com/ory/client-go" "git.happydns.org/happyDomain/config" ) @@ -47,10 +48,10 @@ import ( // @name Authorization // @description Description for what is this security definition being used -func DeclareRoutes(cfg *config.Options, router *gin.Engine) { +func DeclareRoutes(cfg *config.Options, o *ory.APIClient, router *gin.Engine) { apiRoutes := router.Group("/api") - declareAuthenticationRoutes(cfg, apiRoutes) + declareAuthenticationRoutes(cfg, o, apiRoutes) declareProviderSpecsRoutes(apiRoutes) declareResolverRoutes(apiRoutes) declareServiceSpecsRoutes(apiRoutes) @@ -58,7 +59,7 @@ func DeclareRoutes(cfg *config.Options, router *gin.Engine) { DeclareVersionRoutes(apiRoutes) apiAuthRoutes := router.Group("/api") - apiAuthRoutes.Use(authMiddleware(cfg, false)) + apiAuthRoutes.Use(authMiddleware(cfg, o, false)) declareDomainsRoutes(cfg, apiAuthRoutes) declareProvidersRoutes(cfg, apiAuthRoutes) diff --git a/api/user_auth.go b/api/user_auth.go index 7844d20..86e016a 100644 --- a/api/user_auth.go +++ b/api/user_auth.go @@ -31,6 +31,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" + ory "github.com/ory/client-go" "git.happydns.org/happyDomain/config" "git.happydns.org/happyDomain/model" @@ -39,7 +40,7 @@ import ( const NO_AUTH_ACCOUNT = "_no_auth" -func declareAuthenticationRoutes(opts *config.Options, router *gin.RouterGroup) { +func declareAuthenticationRoutes(opts *config.Options, o *ory.APIClient, router *gin.RouterGroup) { router.POST("/auth", func(c *gin.Context) { checkAuth(opts, c) }) @@ -48,7 +49,7 @@ func declareAuthenticationRoutes(opts *config.Options, router *gin.RouterGroup) }) apiAuthRoutes := router.Group("/auth") - apiAuthRoutes.Use(authMiddleware(opts, true)) + apiAuthRoutes.Use(authMiddleware(opts, o, true)) apiAuthRoutes.GET("", func(c *gin.Context) { if _, exists := c.Get("MySession"); exists { @@ -106,7 +107,7 @@ func displayNotAuthToken(opts *config.Options, c *gin.Context) *UserClaims { return nil } - claims, err := completeAuth(opts, c, UserProfile{ + claims, _, err := completeAuth(opts, c, UserProfile{ UserId: []byte{0}, Email: NO_AUTH_ACCOUNT, EmailVerified: true, @@ -199,7 +200,7 @@ func checkAuth(opts *config.Options, c *gin.Context) { return } - claims, err := completeAuth(opts, c, UserProfile{ + claims, _, err := completeAuth(opts, c, UserProfile{ UserId: user.Id, Email: user.Email, EmailVerified: user.EmailVerification != nil, @@ -222,12 +223,12 @@ func checkAuth(opts *config.Options, c *gin.Context) { } } -func completeAuth(opts *config.Options, c *gin.Context, userprofile UserProfile) (*UserClaims, error) { +func completeAuth(opts *config.Options, c *gin.Context, userprofile UserProfile) (*UserClaims, string, error) { // Issue a new JWT token jti := make([]byte, 16) _, err := rand.Read(jti) if err != nil { - return nil, fmt.Errorf("unable to read enough random bytes: %w", err) + return nil, "", fmt.Errorf("unable to read enough random bytes: %w", err) } iat := jwt.NewNumericDate(time.Now()) @@ -243,9 +244,15 @@ func completeAuth(opts *config.Options, c *gin.Context, userprofile UserProfile) token, err := jwtToken.SignedString([]byte(opts.JWTSecretKey)) if err != nil { - return nil, fmt.Errorf("unable to sign user claims: %w", err) + return nil, "", fmt.Errorf("unable to sign user claims: %w", err) } + setCookie(opts, c, token) + + return claims, token, nil +} + +func setCookie(opts *config.Options, c *gin.Context, token string) { c.SetCookie( COOKIE_NAME, // name token, // value @@ -255,6 +262,4 @@ func completeAuth(opts *config.Options, c *gin.Context, userprofile UserProfile) opts.DevProxy == "" && opts.ExternalURL.URL.Scheme != "http", // secure true, // httpOnly ) - - return claims, nil } diff --git a/config/cli.go b/config/cli.go index fc932aa..7e430d1 100644 --- a/config/cli.go +++ b/config/cli.go @@ -40,6 +40,7 @@ func (o *Options) declareFlags() { flag.BoolVar(&o.NoAuth, "no-auth", false, "Disable user access control, use default account") flag.Var(&o.JWTSecretKey, "jwt-secret-key", "Secret key used to verify JWT authentication tokens (a random secret is used if undefined)") flag.Var(&o.ExternalAuth, "external-auth", "Base URL to use for login and registration (use embedded forms if left empty)") + flag.Var(&o.OryKratosServer, "ory-kratos-server", "URL to the Ory Kratos server (default: none, use classical auth)") // Others flags are declared in some other files likes sources, storages, ... when they need specials configurations } diff --git a/config/config.go b/config/config.go index 5405c87..0d74d07 100644 --- a/config/config.go +++ b/config/config.go @@ -67,6 +67,9 @@ type Options struct { // JWTSecretKey stores the private key to sign and verify JWT tokens. JWTSecretKey JWTSecretKey + + // OryKratosServer is the URL to the authentication server. + OryKratosServer URL } // BuildURL appends the given url to the absolute ExternalURL. diff --git a/go.mod b/go.mod index 5fd02ca..a90b624 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,10 @@ require ( github.com/gin-gonic/gin v1.9.1 github.com/go-mail/mail v2.3.1+incompatible github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/google/uuid v1.5.0 github.com/miekg/dns v1.1.57 github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 + github.com/ory/client-go v1.4.7 github.com/ovh/go-ovh v1.4.3 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 @@ -85,7 +87,6 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.4.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect diff --git a/go.sum b/go.sum index ed468fe..8c19b61 100644 --- a/go.sum +++ b/go.sum @@ -212,6 +212,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= @@ -328,6 +330,8 @@ github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/oracle/oci-go-sdk/v32 v32.0.0 h1:SSbzrQO3WRcPJEZ8+b3SFPYsPtkFM96clqrp03lrwbU= github.com/oracle/oci-go-sdk/v32 v32.0.0/go.mod h1:aZc4jC59IuNP3cr5y1nj555QvwojMX2nMJaBiozuuEs= +github.com/ory/client-go v1.4.7 h1:uWPGGM5zVwpSBfcDIhvA6D+bu2YB7zF4STtpAvzkOco= +github.com/ory/client-go v1.4.7/go.mod h1:DfrTIlME7tgrdgpn4UN07s4OJ1SwzHfrkz+C6C0Lbm0= github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0= github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= diff --git a/internal/app/app.go b/internal/app/app.go index 53568c6..30a5781 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -28,6 +28,7 @@ import ( "time" "github.com/gin-gonic/gin" + ory "github.com/ory/client-go" "git.happydns.org/happyDomain/api" "git.happydns.org/happyDomain/config" @@ -37,6 +38,7 @@ import ( type App struct { router *gin.Engine cfg *config.Options + ory *ory.APIClient srv *http.Server } @@ -49,14 +51,20 @@ func NewApp(cfg *config.Options) App { router := gin.New() router.Use(gin.Logger(), gin.Recovery()) - api.DeclareRoutes(cfg, router) - ui.DeclareRoutes(cfg, router) - app := App{ router: router, cfg: cfg, } + if cfg.OryKratosServer.URL != nil { + c := ory.NewConfiguration() + c.Servers = ory.ServerConfigurations{{URL: cfg.OryKratosServer.URL.String()}} + app.ory = ory.NewAPIClient(c) + } + + api.DeclareRoutes(cfg, app.ory, router) + ui.DeclareRoutes(cfg, router) + return app } diff --git a/ui/routes.go b/ui/routes.go index 915bc88..2dd14d7 100644 --- a/ui/routes.go +++ b/ui/routes.go @@ -23,6 +23,7 @@ package ui import ( "flag" + "fmt" "io" "io/ioutil" "log" @@ -63,6 +64,10 @@ func DeclareRoutes(cfg *config.Options, router *gin.Engine) { CustomHeadHTML += fmt.Sprintf(``, MsgHeaderText, MsgHeaderColor) } + if cfg.OryKratosServer.URL != nil { + CustomHeadHTML += fmt.Sprintf(``, cfg.OryKratosServer.URL.String()) + } + if cfg.DevProxy != "" { router.GET("/.svelte-kit/*_", serveOrReverse("", cfg)) router.GET("/node_modules/*_", serveOrReverse("", cfg)) diff --git a/ui/src/lib/components/ForgottenPasswordForm.svelte b/ui/src/lib/components/ForgottenPasswordForm.svelte index 6c480c7..ab7ea69 100644 --- a/ui/src/lib/components/ForgottenPasswordForm.svelte +++ b/ui/src/lib/components/ForgottenPasswordForm.svelte @@ -77,9 +77,6 @@ bind:this={formElm} on:submit|preventDefault={goSendLink} > -

- {$t('email.recover')}. -