mirror of
https://github.com/1f349/lavender.git
synced 2024-12-22 07:34:06 +00:00
Redesign token authentication
This commit is contained in:
parent
46450ca2f1
commit
68f274b45e
@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/1f349/lavender/database"
|
"github.com/1f349/lavender/database"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -18,6 +19,8 @@ type UserAuth struct {
|
|||||||
|
|
||||||
func (u UserAuth) IsGuest() bool { return u.Subject == "" }
|
func (u UserAuth) IsGuest() bool { return u.Subject == "" }
|
||||||
|
|
||||||
|
var ErrAuthHttpError = errors.New("auth http error")
|
||||||
|
|
||||||
func (h *HttpServer) RequireAdminAuthentication(next UserHandler) httprouter.Handle {
|
func (h *HttpServer) RequireAdminAuthentication(next UserHandler) httprouter.Handle {
|
||||||
return h.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
|
return h.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
|
||||||
var roles string
|
var roles string
|
||||||
@ -48,18 +51,29 @@ func (h *HttpServer) RequireAuthentication(next UserHandler) httprouter.Handle {
|
|||||||
|
|
||||||
func (h *HttpServer) OptionalAuthentication(next UserHandler) httprouter.Handle {
|
func (h *HttpServer) OptionalAuthentication(next UserHandler) httprouter.Handle {
|
||||||
return func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
return func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||||
authUser, err := h.internalAuthenticationHandler(req)
|
authUser, err := h.internalAuthenticationHandler(rw, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
if !errors.Is(err, ErrAuthHttpError) {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next(rw, req, params, authUser)
|
next(rw, req, params, authUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) internalAuthenticationHandler(req *http.Request) (UserAuth, error) {
|
func (h *HttpServer) internalAuthenticationHandler(rw http.ResponseWriter, req *http.Request) (UserAuth, error) {
|
||||||
|
// Delete previous login data cookie
|
||||||
|
http.SetCookie(rw, &http.Cookie{
|
||||||
|
Name: "lavender-login-data",
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: -1,
|
||||||
|
Secure: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
|
||||||
var u UserAuth
|
var u UserAuth
|
||||||
err := h.readLoginDataCookie(req, &u)
|
err := h.readLoginAccessCookie(rw, req, &u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// not logged in
|
// not logged in
|
||||||
return UserAuth{}, nil
|
return UserAuth{}, nil
|
||||||
|
21
server/db.go
21
server/db.go
@ -1,22 +1,33 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/1f349/lavender/database"
|
"github.com/1f349/lavender/database"
|
||||||
"github.com/1f349/lavender/logger"
|
"github.com/1f349/lavender/logger"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrDatabaseActionFailed = errors.New("database action failed")
|
||||||
|
|
||||||
// DbTx wraps a database transaction with http error messages and a simple action
|
// DbTx wraps a database transaction with http error messages and a simple action
|
||||||
// function. If the action function returns an error the transaction will be
|
// function. If the action function returns an error the transaction will be
|
||||||
// rolled back. If there is no error then the transaction is committed.
|
// rolled back. If there is no error then the transaction is committed.
|
||||||
func (h *HttpServer) DbTx(rw http.ResponseWriter, action func(db *database.Queries) error) bool {
|
func (h *HttpServer) DbTx(rw http.ResponseWriter, action func(tx *database.Queries) error) bool {
|
||||||
err := action(h.db)
|
logger.Logger.Helper()
|
||||||
if err != nil {
|
if h.DbTxError(action) != nil {
|
||||||
http.Error(rw, "Database error", http.StatusInternalServerError)
|
http.Error(rw, "Database error", http.StatusInternalServerError)
|
||||||
logger.Logger.Helper()
|
|
||||||
logger.Logger.Warn("Database action error", "err", err)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) DbTxError(action func(tx *database.Queries) error) error {
|
||||||
|
logger.Logger.Helper()
|
||||||
|
err := action(h.db)
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Warn("Database action error", "err", err)
|
||||||
|
return ErrDatabaseActionFailed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
156
server/login.go
156
server/login.go
@ -87,7 +87,7 @@ func (h *HttpServer) loginPost(rw http.ResponseWriter, req *http.Request, _ http
|
|||||||
|
|
||||||
// save state for use later
|
// save state for use later
|
||||||
state := login.Config.Namespace + ":" + uuid.NewString()
|
state := login.Config.Namespace + ":" + uuid.NewString()
|
||||||
h.flowState.Set(state, flowStateData{login, req.PostFormValue("redirect")}, time.Now().Add(15*time.Minute))
|
h.flowState.Set(state, flowStateData{loginName, login, req.PostFormValue("redirect")}, time.Now().Add(15*time.Minute))
|
||||||
|
|
||||||
// generate oauth2 config and redirect to authorize URL
|
// generate oauth2 config and redirect to authorize URL
|
||||||
oa2conf := login.OAuth2Config
|
oa2conf := login.OAuth2Config
|
||||||
@ -96,7 +96,7 @@ func (h *HttpServer) loginPost(rw http.ResponseWriter, req *http.Request, _ http
|
|||||||
http.Redirect(rw, req, nextUrl, http.StatusFound)
|
http.Redirect(rw, req, nextUrl, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {
|
func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, userAuth UserAuth) {
|
||||||
flowState, ok := h.flowState.Get(req.FormValue("state"))
|
flowState, ok := h.flowState.Get(req.FormValue("state"))
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(rw, "Invalid flow state", http.StatusBadRequest)
|
http.Error(rw, "Invalid flow state", http.StatusBadRequest)
|
||||||
@ -108,13 +108,29 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionData, err := h.fetchUserInfo(flowState.sso, token)
|
userAuth, err = h.updateExternalUserInfo(req, flowState.sso, token)
|
||||||
if err != nil || sessionData.Subject == "" {
|
if err != nil {
|
||||||
http.Error(rw, "Failed to fetch user info", http.StatusInternalServerError)
|
http.Error(rw, "Failed to update external user info", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.DbTx(rw, func(tx *database.Queries) error {
|
if h.setLoginDataCookie(rw, userAuth, flowState.loginName) {
|
||||||
|
http.Error(rw, "Failed to save login cookie", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if flowState.redirect != "" {
|
||||||
|
req.Form.Set("redirect", flowState.redirect)
|
||||||
|
}
|
||||||
|
h.SafeRedirect(rw, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) updateExternalUserInfo(req *http.Request, sso *issuer.WellKnownOIDC, token *oauth2.Token) (UserAuth, error) {
|
||||||
|
sessionData, err := h.fetchUserInfo(sso, token)
|
||||||
|
if err != nil || sessionData.Subject == "" {
|
||||||
|
return UserAuth{}, fmt.Errorf("failed to fetch user info")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.DbTxError(func(tx *database.Queries) error {
|
||||||
jBytes, err := json.Marshal(sessionData.UserInfo)
|
jBytes, err := json.Marshal(sessionData.UserInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -141,48 +157,51 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _
|
|||||||
Userinfo: string(jBytes),
|
Userinfo: string(jBytes),
|
||||||
Subject: uEmail,
|
Subject: uEmail,
|
||||||
})
|
})
|
||||||
}) {
|
})
|
||||||
return
|
if err != nil {
|
||||||
|
return UserAuth{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// only continues if the above tx succeeds
|
// only continues if the above tx succeeds
|
||||||
auth = sessionData
|
if err := h.DbTxError(func(tx *database.Queries) error {
|
||||||
|
|
||||||
if h.DbTx(rw, func(tx *database.Queries) error {
|
|
||||||
return tx.UpdateUserToken(req.Context(), database.UpdateUserTokenParams{
|
return tx.UpdateUserToken(req.Context(), database.UpdateUserTokenParams{
|
||||||
AccessToken: sql.NullString{String: token.AccessToken, Valid: true},
|
AccessToken: sql.NullString{String: token.AccessToken, Valid: true},
|
||||||
RefreshToken: sql.NullString{String: token.RefreshToken, Valid: true},
|
RefreshToken: sql.NullString{String: token.RefreshToken, Valid: true},
|
||||||
Expiry: sql.NullTime{Time: token.Expiry, Valid: true},
|
Expiry: sql.NullTime{Time: token.Expiry, Valid: true},
|
||||||
Subject: auth.Subject,
|
Subject: sessionData.Subject,
|
||||||
})
|
})
|
||||||
}) {
|
}); err != nil {
|
||||||
return
|
return UserAuth{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.setLoginDataCookie(rw, auth) {
|
return sessionData, nil
|
||||||
http.Error(rw, "Failed to save login cookie", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if flowState.redirect != "" {
|
|
||||||
req.Form.Set("redirect", flowState.redirect)
|
|
||||||
}
|
|
||||||
h.SafeRedirect(rw, req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const oneYear = 365 * 24 * time.Hour
|
const twelveHours = 12 * time.Hour
|
||||||
|
const oneWeek = 7 * 24 * time.Hour
|
||||||
|
|
||||||
type lavenderLoginData struct {
|
type lavenderLoginAccess struct {
|
||||||
UserInfo UserInfoFields `json:"user_info"`
|
UserInfo UserInfoFields `json:"user_info"`
|
||||||
auth.AccessTokenClaims
|
auth.AccessTokenClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l lavenderLoginData) Valid() error { return nil }
|
func (l lavenderLoginAccess) Valid() error { return l.AccessTokenClaims.Valid() }
|
||||||
|
|
||||||
func (l lavenderLoginData) Type() string { return "lavender-login-data" }
|
func (l lavenderLoginAccess) Type() string { return "lavender-login-access" }
|
||||||
|
|
||||||
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, authData UserAuth) bool {
|
type lavenderLoginRefresh struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
auth.RefreshTokenClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l lavenderLoginRefresh) Valid() error { return l.RefreshTokenClaims.Valid() }
|
||||||
|
|
||||||
|
func (l lavenderLoginRefresh) Type() string { return "lavender-login-refresh" }
|
||||||
|
|
||||||
|
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, authData UserAuth, loginName string) bool {
|
||||||
ps := claims.NewPermStorage()
|
ps := claims.NewPermStorage()
|
||||||
gen, err := h.signingKey.GenerateJwt(authData.Subject, uuid.NewString(), jwt.ClaimStrings{h.conf.BaseUrl}, oneYear, lavenderLoginData{
|
accId := uuid.NewString()
|
||||||
|
gen, err := h.signingKey.GenerateJwt(authData.Subject, accId, jwt.ClaimStrings{h.conf.BaseUrl}, twelveHours, lavenderLoginAccess{
|
||||||
UserInfo: authData.UserInfo,
|
UserInfo: authData.UserInfo,
|
||||||
AccessTokenClaims: auth.AccessTokenClaims{Perms: ps},
|
AccessTokenClaims: auth.AccessTokenClaims{Perms: ps},
|
||||||
})
|
})
|
||||||
@ -190,29 +209,92 @@ func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, authData UserAut
|
|||||||
http.Error(rw, "Failed to generate cookie token", http.StatusInternalServerError)
|
http.Error(rw, "Failed to generate cookie token", http.StatusInternalServerError)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
ref, err := h.signingKey.GenerateJwt(authData.Subject, uuid.NewString(), jwt.ClaimStrings{h.conf.BaseUrl}, oneWeek, lavenderLoginRefresh{
|
||||||
|
Login: loginName,
|
||||||
|
RefreshTokenClaims: auth.RefreshTokenClaims{AccessTokenId: accId},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, "Failed to generate cookie token", http.StatusInternalServerError)
|
||||||
|
return true
|
||||||
|
}
|
||||||
http.SetCookie(rw, &http.Cookie{
|
http.SetCookie(rw, &http.Cookie{
|
||||||
Name: "lavender-login-data",
|
Name: "lavender-login-access",
|
||||||
Value: gen,
|
Value: gen,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Expires: time.Now().AddDate(0, 3, 0),
|
|
||||||
Secure: true,
|
Secure: true,
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
http.SetCookie(rw, &http.Cookie{
|
||||||
|
Name: "lavender-login-refresh",
|
||||||
|
Value: ref,
|
||||||
|
Path: "/",
|
||||||
|
Expires: time.Now().AddDate(0, 0, 10),
|
||||||
|
Secure: true,
|
||||||
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) readLoginDataCookie(req *http.Request, u *UserAuth) error {
|
func readJwtCookie[T mjwt.Claims](req *http.Request, cookieName string, signingKey mjwt.Verifier) (mjwt.BaseTypeClaims[T], error) {
|
||||||
loginCookie, err := req.Cookie("lavender-login-data")
|
loginCookie, err := req.Cookie(cookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return mjwt.BaseTypeClaims[T]{}, err
|
||||||
}
|
}
|
||||||
_, b, err := mjwt.ExtractClaims[lavenderLoginData](h.signingKey, loginCookie.Value)
|
_, b, err := mjwt.ExtractClaims[T](signingKey, loginCookie.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return mjwt.BaseTypeClaims[T]{}, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) readLoginAccessCookie(rw http.ResponseWriter, req *http.Request, u *UserAuth) error {
|
||||||
|
loginData, err := readJwtCookie[lavenderLoginAccess](req, "lavender-login-access", h.signingKey)
|
||||||
|
if err != nil {
|
||||||
|
return h.readLoginRefreshCookie(rw, req, u)
|
||||||
}
|
}
|
||||||
*u = UserAuth{
|
*u = UserAuth{
|
||||||
Subject: b.Subject,
|
Subject: loginData.Subject,
|
||||||
UserInfo: b.Claims.UserInfo,
|
UserInfo: loginData.Claims.UserInfo,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) readLoginRefreshCookie(rw http.ResponseWriter, req *http.Request, userAuth *UserAuth) error {
|
||||||
|
refreshData, err := readJwtCookie[lavenderLoginRefresh](req, "lavender-login-refresh", h.signingKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sso := h.manager.FindServiceFromLogin(refreshData.Claims.Login)
|
||||||
|
|
||||||
|
var oauthToken *oauth2.Token
|
||||||
|
|
||||||
|
err = h.DbTxError(func(tx *database.Queries) error {
|
||||||
|
token, err := tx.GetUserToken(req.Context(), refreshData.Subject)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !token.AccessToken.Valid || !token.RefreshToken.Valid || !token.Expiry.Valid {
|
||||||
|
return fmt.Errorf("invalid oauth token")
|
||||||
|
}
|
||||||
|
oauthToken = &oauth2.Token{
|
||||||
|
AccessToken: token.AccessToken.String,
|
||||||
|
RefreshToken: token.RefreshToken.String,
|
||||||
|
Expiry: token.Expiry.Time,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
*userAuth, err = h.updateExternalUserInfo(req, sso, oauthToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.setLoginDataCookie(rw, *userAuth, refreshData.Claims.Login) {
|
||||||
|
http.Error(rw, "Failed to save login cookie", http.StatusInternalServerError)
|
||||||
|
return fmt.Errorf("failed to save login cookie: %w", ErrAuthHttpError)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ func (h *HttpServer) oauthUserAuthorization(rw http.ResponseWriter, req *http.Re
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
auth, err := h.internalAuthenticationHandler(req)
|
auth, err := h.internalAuthenticationHandler(nil, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -38,8 +38,9 @@ type HttpServer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type flowStateData struct {
|
type flowStateData struct {
|
||||||
sso *issuer.WellKnownOIDC
|
loginName string
|
||||||
redirect string
|
sso *issuer.WellKnownOIDC
|
||||||
|
redirect string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHttpServer(conf Conf, db *database.Queries, signingKey mjwt.Signer) *http.Server {
|
func NewHttpServer(conf Conf, db *database.Queries, signingKey mjwt.Signer) *http.Server {
|
||||||
@ -126,7 +127,14 @@ func NewHttpServer(conf Conf, db *database.Queries, signingKey mjwt.Signer) *htt
|
|||||||
}
|
}
|
||||||
if subtle.ConstantTimeCompare([]byte(cookie.Value), []byte(req.PostFormValue("nonce"))) == 1 {
|
if subtle.ConstantTimeCompare([]byte(cookie.Value), []byte(req.PostFormValue("nonce"))) == 1 {
|
||||||
http.SetCookie(rw, &http.Cookie{
|
http.SetCookie(rw, &http.Cookie{
|
||||||
Name: "lavender-login-data",
|
Name: "lavender-login-access",
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: -1,
|
||||||
|
Secure: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
http.SetCookie(rw, &http.Cookie{
|
||||||
|
Name: "lavender-login-refresh",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Secure: true,
|
Secure: true,
|
||||||
|
Loading…
Reference in New Issue
Block a user