Files
hanko/backend/session/session.go

146 lines
4.0 KiB
Go

package session
import (
"fmt"
"github.com/gofrs/uuid"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/teamhanko/hanko/backend/config"
hankoJwk "github.com/teamhanko/hanko/backend/crypto/jwk"
hankoJwt "github.com/teamhanko/hanko/backend/crypto/jwt"
"net/http"
"time"
)
type Manager interface {
GenerateJWT(uuid.UUID) (string, error)
Verify(string) (jwt.Token, error)
GenerateCookie(token string) (*http.Cookie, error)
DeleteCookie() (*http.Cookie, error)
}
// Manager is used to create and verify session JWTs
type manager struct {
jwtGenerator hankoJwt.Generator
sessionLength time.Duration
cookieConfig cookieConfig
issuer string
audience []string
}
type cookieConfig struct {
Name string
Domain string
HttpOnly bool
SameSite http.SameSite
Secure bool
}
// NewManager returns a new Manager which will be used to create and verify sessions JWTs
func NewManager(jwkManager hankoJwk.Manager, config config.Config) (Manager, error) {
signatureKey, err := jwkManager.GetSigningKey()
if err != nil {
return nil, fmt.Errorf("failed to create session generator: %w", err)
}
verificationKeys, err := jwkManager.GetPublicKeys()
if err != nil {
return nil, fmt.Errorf("failed to create session generator: %w", err)
}
g, err := hankoJwt.NewGenerator(signatureKey, verificationKeys)
if err != nil {
return nil, fmt.Errorf("failed to create session generator: %w", err)
}
duration, _ := time.ParseDuration(config.Session.Lifespan) // error can be ignored, value is checked in config validation
sameSite := http.SameSite(0)
switch config.Session.Cookie.SameSite {
case "lax":
sameSite = http.SameSiteLaxMode
case "strict":
sameSite = http.SameSiteStrictMode
case "none":
sameSite = http.SameSiteNoneMode
default:
sameSite = http.SameSiteDefaultMode
}
var audience []string
if config.Session.Audience != nil && len(config.Session.Audience) > 0 {
audience = config.Session.Audience
} else {
audience = []string{config.Webauthn.RelyingParty.Id}
}
return &manager{
jwtGenerator: g,
sessionLength: duration,
issuer: config.Session.Issuer,
cookieConfig: cookieConfig{
Name: config.Session.Cookie.GetName(),
Domain: config.Session.Cookie.Domain,
HttpOnly: config.Session.Cookie.HttpOnly,
SameSite: sameSite,
Secure: config.Session.Cookie.Secure,
},
audience: audience,
}, nil
}
// GenerateJWT creates a new session JWT for the given user
func (m *manager) GenerateJWT(userId uuid.UUID) (string, error) {
issuedAt := time.Now()
expiration := issuedAt.Add(m.sessionLength)
token := jwt.New()
_ = token.Set(jwt.SubjectKey, userId.String())
_ = token.Set(jwt.IssuedAtKey, issuedAt)
_ = token.Set(jwt.ExpirationKey, expiration)
_ = token.Set(jwt.AudienceKey, m.audience)
if m.issuer != "" {
_ = token.Set(jwt.IssuerKey, m.issuer)
}
signed, err := m.jwtGenerator.Sign(token)
if err != nil {
return "", err
}
return string(signed), nil
}
// Verify verifies the given JWT and returns a parsed one if verification was successful
func (m *manager) Verify(token string) (jwt.Token, error) {
parsedToken, err := m.jwtGenerator.Verify([]byte(token))
if err != nil {
return nil, fmt.Errorf("failed to verify session token: %w", err)
}
return parsedToken, nil
}
// GenerateCookie creates a new session cookie for the given user
func (m *manager) GenerateCookie(token string) (*http.Cookie, error) {
return &http.Cookie{
Name: m.cookieConfig.Name,
Value: token,
Domain: m.cookieConfig.Domain,
Path: "/",
Secure: m.cookieConfig.Secure,
HttpOnly: m.cookieConfig.HttpOnly,
SameSite: m.cookieConfig.SameSite,
MaxAge: int(m.sessionLength.Seconds()),
}, nil
}
// DeleteCookie returns a cookie that will expire the cookie on the frontend
func (m *manager) DeleteCookie() (*http.Cookie, error) {
return &http.Cookie{
Name: "hanko",
Value: "",
Domain: m.cookieConfig.Domain,
Path: "/",
Secure: m.cookieConfig.Secure,
HttpOnly: m.cookieConfig.HttpOnly,
SameSite: m.cookieConfig.SameSite,
MaxAge: -1,
}, nil
}