mirror of
https://github.com/1f349/lavender.git
synced 2025-04-15 15:27:55 +01:00
202 lines
5.6 KiB
Go
202 lines
5.6 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/1f349/cache"
|
|
"github.com/1f349/lavender/auth"
|
|
"github.com/1f349/lavender/auth/process"
|
|
"github.com/1f349/lavender/auth/providers"
|
|
"github.com/1f349/lavender/conf"
|
|
"github.com/1f349/lavender/database"
|
|
"github.com/1f349/lavender/issuer"
|
|
"github.com/1f349/lavender/logger"
|
|
"github.com/1f349/lavender/mail"
|
|
"github.com/1f349/lavender/utils"
|
|
"github.com/1f349/lavender/web"
|
|
"github.com/1f349/mjwt"
|
|
"github.com/go-oauth2/oauth2/v4/manage"
|
|
"github.com/go-oauth2/oauth2/v4/server"
|
|
"github.com/julienschmidt/httprouter"
|
|
"golang.org/x/oauth2"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
var errInvalidScope = errors.New("missing required scope")
|
|
|
|
type httpServer struct {
|
|
r *httprouter.Router
|
|
oauthSrv *server.Server
|
|
oauthMgr *manage.Manager
|
|
db *database.Queries
|
|
conf conf.Conf
|
|
signingKey *mjwt.Issuer
|
|
mailSender *mail.Mail
|
|
manager *issuer.Manager
|
|
|
|
// mailLinkCache contains a mapping of verify uuids to user uuids
|
|
mailLinkCache *cache.Cache[mailLinkKey, string]
|
|
|
|
// flowState contains the flow state of 3rd party oauth2
|
|
flowState *cache.Cache[string, flowStateData]
|
|
|
|
authSources []auth.Provider
|
|
authButtons []auth.Button
|
|
formProviderLookup map[string]auth.Form
|
|
}
|
|
|
|
type flowStateData struct {
|
|
loginName string
|
|
sso *issuer.WellKnownOIDC
|
|
redirect string
|
|
}
|
|
|
|
type mailLink byte
|
|
|
|
const (
|
|
mailLinkDelete mailLink = iota
|
|
mailLinkResetPassword
|
|
mailLinkVerifyEmail
|
|
)
|
|
|
|
type mailLinkKey struct {
|
|
action mailLink
|
|
data string
|
|
}
|
|
|
|
func SetupRouter(r *httprouter.Router, config conf.Conf, mailSender *mail.Mail, db *database.Queries, signingKey *mjwt.Issuer) {
|
|
// TODO: move auth provider init to main function
|
|
// TODO: allow dynamically changing the providers based on database information
|
|
// TODO: move oauth setup into oauth provider
|
|
|
|
hs := &httpServer{
|
|
r: r,
|
|
db: db,
|
|
conf: config,
|
|
signingKey: signingKey,
|
|
mailSender: mailSender,
|
|
|
|
mailLinkCache: cache.New[mailLinkKey, string](),
|
|
}
|
|
|
|
var err error
|
|
hs.manager, err = issuer.NewManager(db, config.Namespace)
|
|
if err != nil {
|
|
logger.Logger.Fatal("Failed to load SSO services", "err", err)
|
|
}
|
|
|
|
authPassword := &providers.PasswordLogin{DB: db}
|
|
authOtp := &providers.OtpLogin{DB: db}
|
|
authOAuth := &providers.OAuthLogin{DB: db, BaseUrl: config.BaseUrl, Manager: hs.manager}
|
|
authOAuth.Init()
|
|
authPasskey := &providers.PasskeyLogin{DB: db}
|
|
authInitial := &providers.InitialLogin{DB: db, MyNamespace: config.Namespace, Manager: hs.manager, OAuth: authOAuth}
|
|
|
|
hs.authSources = []auth.Provider{
|
|
authInitial,
|
|
authPassword,
|
|
authOtp,
|
|
authOAuth,
|
|
authPasskey,
|
|
}
|
|
|
|
r.GET("/oauth/:namespace/start", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
|
namespace := params.ByName("namespace")
|
|
if !authOAuth.RedirectToAuthorize(rw, req, namespace) {
|
|
http.Error(rw, "Invalid OAuth namespace", http.StatusBadRequest)
|
|
}
|
|
})
|
|
r.GET("/oauth/:namespace/callback", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
|
namespace := params.ByName("namespace")
|
|
authOAuth.OAuthCallback(rw, req, namespace, func(req *http.Request, sso *issuer.WellKnownOIDC, token *oauth2.Token) (auth.UserAuth, error) {
|
|
resp, err := sso.OAuth2Config.Client(req.Context(), token).Get(sso.UserInfoEndpoint.String())
|
|
if err != nil {
|
|
return auth.UserAuth{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var userInfoJson auth.UserInfoFields
|
|
if err := json.NewDecoder(resp.Body).Decode(&userInfoJson); err != nil {
|
|
return auth.UserAuth{}, err
|
|
}
|
|
subject, ok := userInfoJson.GetString("sub")
|
|
if !ok {
|
|
return auth.UserAuth{}, fmt.Errorf("invalid subject")
|
|
}
|
|
subject += "@" + sso.Config.Namespace
|
|
|
|
return auth.UserAuth{
|
|
Subject: subject,
|
|
Factor: process.StateBasic,
|
|
UserInfo: userInfoJson,
|
|
}, nil
|
|
}, func(rw http.ResponseWriter, authData auth.UserAuth, loginName string) bool {
|
|
// TODO: this should be using the existing auth flow calls
|
|
return hs.setLoginDataCookie(rw, authData, loginName)
|
|
}, func(rw http.ResponseWriter, req *http.Request) {
|
|
// TODO: is this really needed like this?
|
|
utils.SafeRedirect(rw, req)
|
|
})
|
|
})
|
|
|
|
// build slices and maps for quick access to auth interfaces
|
|
hs.authButtons = make([]auth.Button, 0)
|
|
hs.formProviderLookup = make(map[string]auth.Form)
|
|
for _, source := range hs.authSources {
|
|
if button, isButton := source.(auth.Button); isButton {
|
|
hs.authButtons = append(hs.authButtons, button)
|
|
}
|
|
|
|
if form, isForm := source.(auth.Form); isForm {
|
|
hs.formProviderLookup[form.Name()] = form
|
|
}
|
|
}
|
|
|
|
SetupOpenId(r, config.BaseUrl, signingKey)
|
|
r.GET("/", hs.OptionalAuthentication(false, hs.Home))
|
|
r.POST("/logout", hs.RequireAuthentication(hs.logoutPost))
|
|
|
|
// theme styles
|
|
r.GET("/_astro/*filepath", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
|
name := params.ByName("filepath")
|
|
if strings.Contains(name, "..") {
|
|
http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
return
|
|
}
|
|
web.RenderWebAsset(rw, req, path.Join("_astro", name))
|
|
})
|
|
|
|
// login steps
|
|
r.GET("/login", hs.OptionalAuthentication(false, hs.loginGet))
|
|
r.POST("/login", hs.OptionalAuthentication(false, hs.loginPost))
|
|
r.GET("/callback", hs.OptionalAuthentication(false, hs.loginCallback))
|
|
|
|
SetupManageApps(r, hs)
|
|
SetupManageUsers(r, hs)
|
|
SetupOAuth2(r, hs, signingKey, db)
|
|
}
|
|
|
|
func ParseClaims(claims string) map[string]bool {
|
|
m := make(map[string]bool)
|
|
for {
|
|
n := strings.IndexByte(claims, ' ')
|
|
if n == -1 {
|
|
if claims != "" {
|
|
m[claims] = true
|
|
}
|
|
break
|
|
}
|
|
|
|
a := claims[:n]
|
|
claims = claims[n+1:]
|
|
if a != "" {
|
|
m[a] = true
|
|
}
|
|
}
|
|
|
|
return m
|
|
}
|