session login

This commit is contained in:
Ruidy 2024-05-25 16:38:25 +02:00
parent f66ada145a
commit e1766812c4
No known key found for this signature in database
GPG key ID: E00F51288CB857CC
10 changed files with 85 additions and 185 deletions

7
go.mod
View file

@ -6,7 +6,7 @@ require (
github.com/a-h/templ v0.2.680
github.com/getsentry/sentry-go v0.27.0
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/gorilla/sessions v1.1.1
github.com/gorilla/sessions v1.2.2
github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v4 v4.12.0
github.com/labstack/gommon v0.4.2
@ -19,15 +19,16 @@ require (
require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/mux v1.6.2 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/labstack/echo-contrib v0.17.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect

9
go.sum
View file

@ -17,12 +17,18 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
@ -37,6 +43,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/labstack/echo-contrib v0.17.1 h1:7I/he7ylVKsDUieaGRZ9XxxTYOjfQwVzHzUYrNykfCU=
github.com/labstack/echo-contrib v0.17.1/go.mod h1:SnsCZtwHBAZm5uBSAtQtXQHI3wqEA73hvTn0bYMKnZA=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
@ -61,6 +69,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=

View file

@ -1,19 +1,9 @@
package auth
import (
"sort"
"github.com/gorilla/sessions"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/google"
)
type Service struct {
secret string
googleClientId string
googleSecret string
googleRedirectUrl string
secret string
admin string
adminSecret string
}
type ProviderIndex struct {
@ -21,28 +11,14 @@ type ProviderIndex struct {
Providers []string
}
func NewService(secret, googleClientId, googleSecret, googleRedirectUrl string) *Service {
func NewService(secret, admin, adminSecret string) *Service {
return &Service{
secret,
googleClientId,
googleSecret,
googleRedirectUrl,
admin,
adminSecret,
}
}
func (as Service) GetProviderIndex() *ProviderIndex {
goth.UseProviders(google.New(as.googleClientId, as.googleSecret, as.googleRedirectUrl))
m := map[string]string{
"google": "Google",
}
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
gothic.Store = sessions.NewCookieStore([]byte(as.secret))
return &ProviderIndex{Providers: keys, ProvidersMap: m}
func (as *Service) Authenticate(email, password string) bool {
return email == as.admin && password == as.adminSecret
}

View file

@ -1,81 +1,38 @@
package server
import (
"fmt"
"net/http"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)
const (
cookieName = "rentuuid"
routeLogin = "/login"
routeLogin = "/"
)
var validityTime = time.Now().Add(time.Hour * 24)
type Claims struct {
jwt.RegisteredClaims
}
func MakeAuthMiddleware(secretKey string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cookie, err := c.Cookie(cookieName)
if err != nil {
return c.Redirect(http.StatusSeeOther, routeLogin)
}
signedToken := cookie.Value
token, err := jwt.Parse(
signedToken,
func(*jwt.Token) (interface{}, error) {
return []byte(secretKey), nil
},
jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name}),
)
if err != nil {
return c.Redirect(http.StatusSeeOther, routeLogin)
}
if !token.Valid {
return c.Redirect(http.StatusSeeOther, routeLogin)
if c.Request().RequestURI == routeLogin {
return next(c)
}
_, err = token.Claims.GetSubject()
if err != nil {
return c.Redirect(http.StatusSeeOther, routeLogin)
s, err := readSession(c)
if s != "foo" || err != nil {
return c.Redirect(http.StatusUnauthorized, routeLogin)
}
return next(c)
}
}
}
// TODO: refactor to use a `AuthService`
func writeCookie(c echo.Context, email string) error {
claims := &Claims{
jwt.RegisteredClaims{
Subject: email,
ExpiresAt: jwt.NewNumericDate(validityTime),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := token.SignedString([]byte(os.Getenv("SECRET_KEY")))
func readSession(c echo.Context) (string, error) {
sess, err := session.Get(sessionName, c)
if err != nil {
return err
return "", err
}
cookie := new(http.Cookie)
cookie.Name = cookieName
cookie.Value = signedToken
cookie.Expires = validityTime
cookie.HttpOnly = true
cookie.Domain = os.Getenv("DOMAIN")
cookie.Secure = true
cookie.SameSite = http.SameSiteStrictMode
c.SetCookie(cookie)
return nil
return fmt.Sprintf("foo=%v\n", sess.Values["foo"]), nil
}

View file

@ -1,77 +1,49 @@
package server
import (
"fmt"
"html/template"
"net/http"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/markbates/goth/gothic"
"github.com/labstack/gommon/log"
"github.com/rjNemo/rentease/internal/auth"
"github.com/rjNemo/rentease/internal/view"
)
var indexTemplate = `{{range $key,$value:=.Providers}}
<p><a href="/auth?provider={{$value}}">Log in with {{index $.ProvidersMap $value}}</a></p>
{{end}}`
const (
sessionName = "rentease"
sessionAge = 86400 * 7 // 7 days
)
var userTemplate = `
<p><a href="/logout?provider={{.Provider}}">logout</a></p>
<p>Name: {{.Name}} [{{.LastName}}, {{.FirstName}}]</p>
<p>Email: {{.Email}}</p>
<p>NickName: {{.NickName}}</p>
<p>Location: {{.Location}}</p>
<p>AvatarURL: {{.AvatarURL}} <img src="{{.AvatarURL}}"></p>
<p>Description: {{.Description}}</p>
<p>UserID: {{.UserID}}</p>
<p>AccessToken: {{.AccessToken}}</p>
<p>ExpiresAt: {{.ExpiresAt}}</p>
<p>RefreshToken: {{.RefreshToken}}</p>
`
func handleProviderCallback() echo.HandlerFunc {
func handleLoginPage() echo.HandlerFunc {
return func(c echo.Context) error {
res := c.Response()
req := c.Request()
user, err := gothic.CompleteUserAuth(res, req)
return renderTempl(c, http.StatusOK, view.Login())
}
}
func handleLogin(as *auth.Service) echo.HandlerFunc {
return func(c echo.Context) error {
sess, err := session.Get(sessionName, c)
if err != nil {
fmt.Fprintln(res, err)
return nil
log.Warn(err)
return err
}
t, _ := template.New("foo").Parse(userTemplate)
return t.Execute(res, user)
}
}
func handleProviderLogout() echo.HandlerFunc {
return func(c echo.Context) error {
res := c.Response()
req := c.Request()
err := gothic.Logout(res, req)
res.Header().Set("Location", "/")
res.WriteHeader(http.StatusTemporaryRedirect)
return err
}
}
func handleProvider() echo.HandlerFunc {
return func(c echo.Context) error {
res := c.Response()
req := c.Request()
// try to get the user without re-authenticating
if gothUser, err := gothic.CompleteUserAuth(res, req); err == nil {
t, _ := template.New("foo").Parse(userTemplate)
return t.Execute(res, gothUser)
} else {
gothic.BeginAuthHandler(res, req)
return nil
sess.Options = &sessions.Options{
Path: "/",
MaxAge: sessionAge,
HttpOnly: true,
}
}
}
func handleLoginPage(as *auth.Service) echo.HandlerFunc {
return func(c echo.Context) error {
return renderTempl(c, http.StatusOK, view.Login(as.GetProviderIndex()))
if !as.Authenticate(c.FormValue("email"), c.FormValue("password")) {
return c.Redirect(http.StatusTemporaryRedirect, routeLogin)
}
sess.Values["foo"] = "bar"
if err := sess.Save(c.Request(), c.Response()); err != nil {
log.Warn(err)
return err
}
return c.Redirect(http.StatusSeeOther, "/bookings")
}
}

View file

@ -10,14 +10,11 @@ import (
func (s Server) MountHandlers() {
// public
s.Router.GET("/debug/pprof/*", echo.WrapHandler(http.DefaultServeMux))
s.Router.POST("/", handleExtension())
// authentication
s.Router.GET("/", handleLoginPage(s.as))
s.Router.GET("/auth", handleProvider())
s.Router.GET("/auth/callback", handleProviderCallback())
s.Router.GET("/logout", handleProviderLogout())
s.Router.GET("/", handleLoginPage())
s.Router.POST("/", handleLogin(s.as))
// admin
g := s.Router.Group("")
g.Use(MakeAuthMiddleware(s.secretKey))
g.GET("/bookings", handleListBookingPage(s.bs, s.hc))
g.GET("/bookings/new", handleNewBookingPage(s.hc))
g.POST("/bookings/new", handleCreateBooking(s.bs))

View file

@ -14,6 +14,8 @@ import (
"github.com/a-h/templ"
"github.com/getsentry/sentry-go"
sentryecho "github.com/getsentry/sentry-go/echo"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
@ -83,7 +85,7 @@ func New(bs *booking.Service, as *auth.Service, ps *pdf.PdfService, hc *config.H
}
s := &Server{
Router: NewRouter(*option.fs, *option.debug),
Router: NewRouter(*option.fs, *option.debug, *option.secretKey),
bs: bs,
as: as,
ps: ps,
@ -125,7 +127,7 @@ func renderTempl(c echo.Context, status int, t templ.Component) error {
return nil
}
func NewRouter(fs embed.FS, debug bool) *echo.Echo {
func NewRouter(fs embed.FS, debug bool, secret string) *echo.Echo {
e := echo.New()
// config
e.HideBanner = true
@ -153,9 +155,10 @@ func NewRouter(fs embed.FS, debug bool) *echo.Echo {
e.Use(middleware.Recover())
e.Use(middleware.Secure())
e.Use(middleware.Gzip())
e.Use(middleware.CSRF())
// e.Use(middleware.CSRF())
e.Use(sentryecho.New(sentryecho.Options{}))
e.Use(SentryTracingMiddleware)
e.Use(session.Middleware(sessions.NewCookieStore([]byte(secret))))
// static assets
e.StaticFS("/static", echo.MustSubFS(fs, "assets"))

View file

@ -1,32 +1,19 @@
package view
import (
"github.com/rjNemo/rentease/internal/auth"
"github.com/rjNemo/rentease/internal/view/layout"
)
// inject the providers into the template even if we don't use it
templ Login(providers *auth.ProviderIndex) {
@layout.PublicLayout() {
templ Login() {
@layout.BaseLayout() {
<main class="container">
<section>
<h1>Login</h1>
<button class="gsi-material-button" onclick="location.href='/auth?provider=google'">
<div class="gsi-material-button-state"></div>
<div class="gsi-material-button-content-wrapper">
<div class="gsi-material-button-icon">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" xmlns:xlink="http://www.w3.org/1999/xlink" style="display: block;">
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"></path>
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"></path>
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"></path>
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"></path>
<path fill="none" d="M0 0h48v48H0z"></path>
</svg>
</div>
<span class="gsi-material-button-contents">Continue with Google</span>
<span style="display: none;">Continue with Google</span>
</div>
</button>
<h1>Welcome</h1>
<form hx-post="/">
<input type="email" name="email" placeholder="john@email.com" aria-label="email" autocomplete="email" autofocus required=""/>
<input type="password" name="password" placeholder="p4Ssw0rD" aria-label="password" autocomplete="password" required=""/>
<button type="submit">Log in</button>
</form>
</section>
</main>
}

View file

@ -11,12 +11,10 @@ import "io"
import "bytes"
import (
"github.com/rjNemo/rentease/internal/auth"
"github.com/rjNemo/rentease/internal/view/layout"
)
// inject the providers into the template even if we don't use it
func Login(providers *auth.ProviderIndex) templ.Component {
func Login() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
@ -35,7 +33,7 @@ func Login(providers *auth.ProviderIndex) templ.Component {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<main class=\"container\"><section><h1>Login</h1><button class=\"gsi-material-button\" onclick=\"location.href=&#39;/auth?provider=google&#39;\"><div class=\"gsi-material-button-state\"></div><div class=\"gsi-material-button-content-wrapper\"><div class=\"gsi-material-button-icon\"><svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 48 48\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" style=\"display: block;\"><path fill=\"#EA4335\" d=\"M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z\"></path> <path fill=\"#4285F4\" d=\"M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z\"></path> <path fill=\"#FBBC05\" d=\"M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z\"></path> <path fill=\"#34A853\" d=\"M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z\"></path> <path fill=\"none\" d=\"M0 0h48v48H0z\"></path></svg></div><span class=\"gsi-material-button-contents\">Continue with Google</span> <span style=\"display: none;\">Continue with Google</span></div></button></section></main>")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<main class=\"container\"><section><h1>Welcome</h1><form hx-post=\"/\"><input type=\"email\" name=\"email\" placeholder=\"john@email.com\" aria-label=\"email\" autocomplete=\"email\" autofocus required=\"\"> <input type=\"password\" name=\"password\" placeholder=\"p4Ssw0rD\" aria-label=\"password\" autocomplete=\"password\" required=\"\"> <button type=\"submit\">Log in</button></form></section></main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -44,7 +42,7 @@ func Login(providers *auth.ProviderIndex) templ.Component {
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.PublicLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
templ_7745c5c3_Err = layout.BaseLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View file

@ -62,7 +62,7 @@ func run(ctx context.Context, getEnv func(string) string) error {
return fmt.Errorf("error starting pdf service %s", err)
}
as := auth.NewService(os.Getenv("SESSION_SECRET"), getEnv("GOOGLE_KEY"), getEnv("GOOGLE_SECRET"), getEnv("GOOGLE_REDIRECT_URL"))
as := auth.NewService(os.Getenv("SESSION_SECRET"), getEnv("ADMIN"), getEnv("ADMIN_SECRET"))
p := getEnv("PORT")
port, err := strconv.Atoi(p)