Skip to content

Commit

Permalink
Refactor session to use gorilla like sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
nemunaire committed May 23, 2024
1 parent e99a604 commit 3d9aecf
Show file tree
Hide file tree
Showing 16 changed files with 455 additions and 263 deletions.
18 changes: 3 additions & 15 deletions admin/db-sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@
package admin

import (
"encoding/base64"
"net/http"

"github.com/gin-gonic/gin"

"git.happydns.org/happyDomain/config"
"git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/storage"
)

Expand All @@ -47,13 +45,7 @@ func deleteSessions(c *gin.Context) {
}

func sessionHandler(c *gin.Context) {
sessionid, err := base64.StdEncoding.DecodeString(c.Param("sessionid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": err.Error()})
return
}

session, err := storage.MainStore.GetSession(sessionid)
session, err := storage.MainStore.GetSession(c.Param("sessionid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": err.Error()})
return
Expand All @@ -65,13 +57,9 @@ func sessionHandler(c *gin.Context) {
}

func getSession(c *gin.Context) {
session := c.MustGet("session").(*happydns.Session)

c.JSON(http.StatusOK, session)
c.JSON(http.StatusOK, c.MustGet("session"))
}

func deleteSession(c *gin.Context) {
session := c.MustGet("session").(*happydns.Session)

ApiResponse(c, true, storage.MainStore.DeleteSession(session))
ApiResponse(c, true, storage.MainStore.DeleteSession(c.Param("sessionid")))
}
154 changes: 64 additions & 90 deletions api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import (
"fmt"
"log"
"net/http"
"strings"
"time"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"

Expand All @@ -50,8 +50,6 @@ type UserClaims struct {
jwt.RegisteredClaims
}

const COOKIE_NAME = "happydomain_session"

var signingMethod = jwt.SigningMethodHS512

func updateUserFromClaims(user *happydns.User, claims *UserClaims) {
Expand Down Expand Up @@ -98,35 +96,6 @@ func retrieveUserFromClaims(claims *UserClaims) (user *happydns.User, err error)
return
}

func retrieveSessionFromClaims(claims *UserClaims, user *happydns.User, session_id []byte) (session *happydns.Session, err error) {
session, err = storage.MainStore.GetSession(session_id)
if err != nil {
// The session doesn't exists yet: create it!
session = &happydns.Session{
Id: session_id,
IdUser: claims.Profile.UserId,
IssuedAt: time.Now(),
}

err = storage.MainStore.UpdateSession(session)
if err != nil {
err = fmt.Errorf("has a correct JWT, but an error occured when trying to create the session: %w", err)
return
}

// Update user's data
updateUserFromClaims(user, claims)

err = storage.MainStore.UpdateUser(user)
if err != nil {
err = fmt.Errorf("has a correct JWT, session has been created, but an error occured when trying to update the user's information: %w", err)
return
}
}

return
}

func requireLogin(opts *config.Options, c *gin.Context, msg string) {
if opts.ExternalAuth.URL != nil {
customurl := *opts.ExternalAuth.URL
Expand All @@ -141,86 +110,91 @@ func requireLogin(opts *config.Options, c *gin.Context, msg string) {

func authMiddleware(opts *config.Options, optional bool) gin.HandlerFunc {
return func(c *gin.Context) {
var token string
// Load user from session
session := sessions.Default(c)

// Retrieve the token from cookie or header
if cookie, err := c.Cookie(COOKIE_NAME); err == nil {
var userid happydns.Identifier
if iu, ok := session.Get("iduser").([]uint8); ok {
userid = happydns.Identifier(iu)
}

// Authentication through JWT
var token string
if c.GetHeader("X-User-Token") != "" {
token = c.GetHeader("X-User-Token")
} else if cookie, err := c.Cookie("happydomain-account"); err == nil {
token = cookie
} else if flds := strings.Fields(c.GetHeader("Authorization")); len(flds) == 2 && flds[0] == "Bearer" {
token = flds[1]
}
if len(opts.JWTSecretKey) > 0 && len(token) > 0 {
// Validate the token and retrieve claims
claims := &UserClaims{}
_, err := jwt.ParseWithClaims(token, claims,
func(token *jwt.Token) (interface{}, error) {
return []byte(opts.JWTSecretKey), nil
}, jwt.WithValidMethods([]string{signingMethod.Name}))
if err != nil {
if opts.NoAuth {
claims = displayNotAuthToken(opts, c)
}

// Stop here if there is no cookie
if len(token) == 0 {
if optional {
c.Next()
} else {
requireLogin(opts, c, "No authorization token found in cookie nor in Authorization header.")
log.Printf("%s provide a bad JWT claims: %s", c.ClientIP(), err.Error())
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
return
}
return
}

// Validate the token and retrieve claims
claims := &UserClaims{}
_, err := jwt.ParseWithClaims(token, claims,
func(token *jwt.Token) (interface{}, error) {
return []byte(opts.JWTSecretKey), nil
}, jwt.WithValidMethods([]string{signingMethod.Name}))
if err != nil {
if opts.NoAuth {
claims = displayNotAuthToken(opts, c)
// Check that required fields are filled
if claims == nil || len(claims.Profile.UserId) == 0 {
log.Printf("%s: no UserId found in JWT claims", c.ClientIP())
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
return
}

log.Printf("%s provide a bad JWT claims: %s", c.ClientIP(), err.Error())
c.SetCookie(COOKIE_NAME, "", -1, opts.BaseURL+"/", "", opts.DevProxy == "", true)
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
return
}
if claims.Profile.Email == "" {
log.Printf("%s: no Email found in JWT claims", c.ClientIP())
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
return
}

// Check that required fields are filled
if claims == nil || len(claims.Profile.UserId) == 0 {
log.Printf("%s: no UserId found in JWT claims", c.ClientIP())
c.SetCookie(COOKIE_NAME, "", -1, opts.BaseURL+"/", "", opts.DevProxy == "", true)
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
return
// Retrieve corresponding user
user, err := retrieveUserFromClaims(claims)
userid = user.Id

if userid != nil {
if userid == nil || userid.IsEmpty() || !userid.Equals(user.Id) {
completeAuth(opts, c, claims.Profile)
session.Clear()
session.Set("iduser", user.Id)
err = session.Save()
if err != nil {
log.Printf("%s: unable to recreate session: %s", c.ClientIP(), err.Error())
requireLogin(opts, c, "Something went wrong with your session. Please contact your administrator.")
return
}
userid = user.Id
}
}
}

if claims.Profile.Email == "" {
log.Printf("%s: no Email found in JWT claims", c.ClientIP())
c.SetCookie(COOKIE_NAME, "", -1, opts.BaseURL+"/", "", opts.DevProxy == "", true)
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
// Stop here if there is no cookie
if userid == nil {
if optional {
c.Next()
} else {
requireLogin(opts, c, "No authorization token found in cookie nor in Authorization header.")
}
return
}

// Retrieve corresponding user
user, err := retrieveUserFromClaims(claims)
user, err := storage.MainStore.GetUser(userid)
if err != nil {
log.Printf("%s %s", c.ClientIP(), err.Error())
c.SetCookie(COOKIE_NAME, "", -1, opts.BaseURL+"/", "", opts.DevProxy == "", true)
requireLogin(opts, c, "Something went wrong with your session. Please reconnect.")
requireLogin(opts, c, "Unable to retrieve your user. Please reauthenticate.")
return
}

c.Set("LoggedUser", user)

// Retrieve the session
session_id := append([]byte(claims.Profile.UserId), []byte(claims.ID)...)
session, err := retrieveSessionFromClaims(claims, user, session_id)
if err != nil {
log.Printf("%s %s", c.ClientIP(), err.Error())
c.SetCookie(COOKIE_NAME, "", -1, opts.BaseURL+"/", "", opts.DevProxy == "", true)
requireLogin(opts, c, "Your session has expired. Please reconnect.")
return
}

c.Set("MySession", session)

// We are now ready to continue
c.Next()

// On return, check if the session has changed
if session.HasChanged() {
storage.MainStore.UpdateSession(session)
}
}
}
5 changes: 3 additions & 2 deletions api/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
package api

import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"

"git.happydns.org/happyDomain/config"
Expand All @@ -44,7 +45,7 @@ type FormState struct {
}

func formDoState(cfg *config.Options, c *gin.Context, fs *FormState, data interface{}, defaultForm func(interface{}) *forms.CustomForm) (form *forms.CustomForm, d map[string]interface{}, err error) {
session := c.MustGet("MySession").(*happydns.Session)
session := sessions.Default(c)

csf, ok := data.(forms.CustomSettingsForm)
if !ok {
Expand All @@ -55,7 +56,7 @@ func formDoState(cfg *config.Options, c *gin.Context, fs *FormState, data interf
}
return
} else {
return csf.DisplaySettingsForm(fs.State, cfg, session, func() string {
return csf.DisplaySettingsForm(fs.State, cfg, &session, func() string {
return fs.Recall
})
}
Expand Down
49 changes: 14 additions & 35 deletions api/user_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@
package api

import (
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"net/http"
"time"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"

"git.happydns.org/happyDomain/config"
"git.happydns.org/happyDomain/internal/session"
"git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/storage"
)
Expand All @@ -51,7 +51,7 @@ func declareAuthenticationRoutes(opts *config.Options, router *gin.RouterGroup)
apiAuthRoutes.Use(authMiddleware(opts, true))

apiAuthRoutes.GET("", func(c *gin.Context) {
if _, exists := c.Get("MySession"); exists {
if _, exists := c.Get("LoggedUser"); exists {
displayAuthToken(c)
} else {
displayNotAuthToken(opts, c)
Expand Down Expand Up @@ -140,7 +140,7 @@ func displayNotAuthToken(opts *config.Options, c *gin.Context) *UserClaims {
// @Router /auth/logout [post]
func logout(opts *config.Options, c *gin.Context) {
c.SetCookie(
COOKIE_NAME,
session.COOKIE_NAME,
"",
-1,
opts.BaseURL+"/",
Expand Down Expand Up @@ -194,7 +194,7 @@ func checkAuth(opts *config.Options, c *gin.Context) {
}

if user.EmailVerification == nil {
log.Printf("%s tries to login as %q, but sent an invalid password", c.ClientIP(), lf.Email)
log.Printf("%s tries to login as %q, but has not verified email", c.ClientIP(), lf.Email)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": "Please validate your e-mail address before your first login.", "href": "/email-validation"})
return
}
Expand Down Expand Up @@ -223,38 +223,17 @@ func checkAuth(opts *config.Options, c *gin.Context) {
}

func completeAuth(opts *config.Options, c *gin.Context, userprofile UserProfile) (*UserClaims, 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)
}

iat := jwt.NewNumericDate(time.Now())
claims := &UserClaims{
userprofile,
jwt.RegisteredClaims{
IssuedAt: iat,
ID: base64.StdEncoding.EncodeToString(jti),
},
}
jwtToken := jwt.NewWithClaims(signingMethod, claims)
jwtToken.Header["kid"] = "1"
session := sessions.Default(c)

token, err := jwtToken.SignedString([]byte(opts.JWTSecretKey))
session.Clear()
session.Set("iduser", userprofile.UserId)
err := session.Save()
if err != nil {
return nil, fmt.Errorf("unable to sign user claims: %w", err)
return nil, err
}

c.SetCookie(
COOKIE_NAME, // name
token, // value
30*24*3600, // maxAge
opts.BaseURL+"/", // path
"", // domain
opts.DevProxy == "" && opts.ExternalURL.URL.Scheme != "http", // secure
true, // httpOnly
)

return claims, nil
return &UserClaims{
userprofile,
jwt.RegisteredClaims{},
}, nil
}
Loading

0 comments on commit 3d9aecf

Please sign in to comment.