lavender/server/server.go

300 lines
8.7 KiB
Go
Raw Normal View History

2023-10-01 21:44:49 +01:00
package server
import (
2024-02-07 01:18:17 +00:00
"crypto/subtle"
"encoding/json"
2023-10-03 23:20:28 +01:00
"github.com/1f349/cache"
2024-02-07 01:18:17 +00:00
clientStore "github.com/1f349/lavender/client-store"
"github.com/1f349/lavender/database"
2023-10-01 21:44:49 +01:00
"github.com/1f349/lavender/issuer"
2024-05-13 20:33:25 +01:00
"github.com/1f349/lavender/logger"
2024-02-07 01:18:17 +00:00
"github.com/1f349/lavender/openid"
"github.com/1f349/lavender/pages"
2024-02-07 01:18:17 +00:00
scope2 "github.com/1f349/lavender/scope"
2023-11-03 07:39:58 +00:00
"github.com/1f349/mjwt"
2024-02-07 01:18:17 +00:00
"github.com/go-oauth2/oauth2/v4/errors"
"github.com/go-oauth2/oauth2/v4/manage"
"github.com/go-oauth2/oauth2/v4/server"
"github.com/go-oauth2/oauth2/v4/store"
2023-10-01 21:44:49 +01:00
"github.com/julienschmidt/httprouter"
"net/http"
2024-02-07 01:18:17 +00:00
"net/url"
"path"
2023-12-14 23:50:09 +00:00
"strings"
2023-10-01 21:44:49 +01:00
"time"
)
2024-02-07 01:18:17 +00:00
var errInvalidScope = errors.New("missing required scope")
2023-10-01 21:44:49 +01:00
type HttpServer struct {
2024-02-07 01:18:17 +00:00
r *httprouter.Router
oauthSrv *server.Server
oauthMgr *manage.Manager
db *database.DB
conf Conf
signingKey mjwt.Signer
manager *issuer.Manager
flowState *cache.Cache[string, flowStateData]
2023-10-03 23:20:28 +01:00
}
type flowStateData struct {
sso *issuer.WellKnownOIDC
redirect string
2023-10-01 21:44:49 +01:00
}
2024-02-07 01:18:17 +00:00
func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Server {
2023-10-01 21:44:49 +01:00
r := httprouter.New()
contentCache := time.Now()
2023-10-01 21:44:49 +01:00
// remove last slash from baseUrl
{
2023-10-08 15:24:59 +01:00
l := len(conf.BaseUrl)
if conf.BaseUrl[l-1] == '/' {
conf.BaseUrl = conf.BaseUrl[:l-1]
}
}
2024-02-07 01:18:17 +00:00
openIdConf := openid.GenConfig(conf.BaseUrl, []string{"openid", "name", "username", "profile", "email", "birthdate", "age", "zoneinfo", "locale"}, []string{"sub", "name", "preferred_username", "profile", "picture", "website", "email", "email_verified", "gender", "birthdate", "zoneinfo", "locale", "updated_at"})
openIdBytes, err := json.Marshal(openIdConf)
if err != nil {
2024-05-13 20:33:25 +01:00
logger.Logger.Fatal("Failed to generate OpenID configuration", "err", err)
2024-02-07 01:18:17 +00:00
}
oauthManager := manage.NewDefaultManager()
oauthSrv := server.NewServer(server.NewConfig(), oauthManager)
2023-10-01 21:44:49 +01:00
hs := &HttpServer{
2024-02-07 01:18:17 +00:00
r: httprouter.New(),
oauthSrv: oauthSrv,
oauthMgr: oauthManager,
db: db,
conf: conf,
signingKey: signingKey,
flowState: cache.New[string, flowStateData](),
2023-11-15 09:21:09 +00:00
}
2024-02-07 01:18:17 +00:00
hs.manager, err = issuer.NewManager(conf.SsoServices)
2023-11-15 09:21:09 +00:00
if err != nil {
2024-05-13 20:33:25 +01:00
logger.Logger.Fatal("Failed to reload SSO service manager", "err", err)
2023-10-01 21:44:49 +01:00
}
2024-02-07 01:18:17 +00:00
oauthManager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
oauthManager.MustTokenStorage(store.NewMemoryTokenStore())
oauthManager.MapAccessGenerate(NewJWTAccessGenerate(hs.signingKey, db))
2024-02-07 01:18:17 +00:00
oauthManager.MapClientStorage(clientStore.New(db))
oauthSrv.SetClientInfoHandler(func(req *http.Request) (clientID, clientSecret string, err error) {
cId, cSecret, err := server.ClientBasicHandler(req)
if cId == "" && cSecret == "" {
cId, cSecret, err = server.ClientFormHandler(req)
}
if err != nil {
return "", "", err
}
return cId, cSecret, nil
})
oauthSrv.SetUserAuthorizationHandler(hs.oauthUserAuthorization)
oauthSrv.SetAuthorizeScopeHandler(func(rw http.ResponseWriter, req *http.Request) (scope string, err error) {
var form url.Values
if req.Method == http.MethodPost {
form = req.PostForm
} else {
form = req.URL.Query()
}
a := form.Get("scope")
if !scope2.ScopesExist(a) {
return "", errInvalidScope
}
return a, nil
})
2024-02-15 12:41:39 +00:00
addIdTokenSupport(oauthSrv, db, signingKey)
2024-02-07 01:18:17 +00:00
r.GET("/.well-known/openid-configuration", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
2023-10-01 21:44:49 +01:00
rw.WriteHeader(http.StatusOK)
2024-02-07 01:18:17 +00:00
_, _ = rw.Write(openIdBytes)
2023-10-01 21:44:49 +01:00
})
2024-02-07 01:18:17 +00:00
r.GET("/", hs.OptionalAuthentication(hs.Home))
2023-12-05 18:10:47 +00:00
2024-02-07 01:18:17 +00:00
// login
2024-02-07 10:54:37 +00:00
r.GET("/login", hs.OptionalAuthentication(hs.loginGet))
r.POST("/login", hs.OptionalAuthentication(hs.loginPost))
r.GET("/callback", hs.OptionalAuthentication(hs.loginCallback))
2024-02-07 01:18:17 +00:00
r.POST("/logout", hs.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
2024-02-21 09:16:38 +00:00
cookie, err := req.Cookie("lavender-nonce")
2024-02-15 15:09:14 +00:00
if err != nil {
http.Error(rw, "Missing nonce", http.StatusBadRequest)
2024-02-07 01:18:17 +00:00
return
}
2024-02-15 15:09:14 +00:00
if subtle.ConstantTimeCompare([]byte(cookie.Value), []byte(req.PostFormValue("nonce"))) == 1 {
2024-02-07 01:18:17 +00:00
http.SetCookie(rw, &http.Cookie{
2024-02-21 09:16:38 +00:00
Name: "lavender-login-data",
2024-02-07 01:18:17 +00:00
Path: "/",
MaxAge: -1,
Secure: true,
2024-02-15 14:44:58 +00:00
SameSite: http.SameSiteLaxMode,
2024-02-07 01:18:17 +00:00
})
http.Redirect(rw, req, "/", http.StatusFound)
return
}
http.Error(rw, "Logout failed", http.StatusInternalServerError)
}))
// theme styles
r.GET("/assets/*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
}
out := pages.RenderCss(path.Join("assets", name))
http.ServeContent(rw, req, path.Base(name), contentCache, out)
2023-12-17 22:46:58 +00:00
})
2024-02-07 01:18:17 +00:00
// management pages
r.GET("/manage/apps", hs.RequireAuthentication(hs.ManageAppsGet))
r.GET("/manage/apps/create", hs.RequireAuthentication(hs.ManageAppsCreateGet))
r.POST("/manage/apps", hs.RequireAuthentication(hs.ManageAppsPost))
2024-02-07 01:18:17 +00:00
r.GET("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersGet))
r.POST("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersPost))
2023-12-14 23:41:39 +00:00
2024-02-07 01:18:17 +00:00
// oauth pages
r.GET("/authorize", hs.RequireAuthentication(hs.authorizeEndpoint))
r.POST("/authorize", hs.RequireAuthentication(hs.authorizeEndpoint))
r.POST("/token", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
if err := oauthSrv.HandleTokenRequest(rw, req); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
2023-12-05 18:10:47 +00:00
})
userInfoRequest := func(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
rw.Header().Set("Access-Control-Allow-Credentials", "true")
rw.Header().Set("Access-Control-Allow-Headers", "Authorization,Content-Type")
rw.Header().Set("Access-Control-Allow-Origin", strings.TrimSuffix(req.Referer(), "/"))
rw.Header().Set("Access-Control-Allow-Methods", "GET")
if req.Method == http.MethodOptions {
return
}
2024-02-07 01:18:17 +00:00
token, err := oauthSrv.ValidationBearerToken(req)
if err != nil {
http.Error(rw, "403 Forbidden", http.StatusForbidden)
return
}
userId := token.GetUserID()
sso := hs.manager.FindServiceFromLogin(userId)
if sso == nil {
http.Error(rw, "Invalid user", http.StatusBadRequest)
return
}
var user *database.User
if hs.DbTx(rw, func(tx *database.Tx) (err error) {
user, err = tx.GetUser(userId)
return err
}) {
return
}
var userInfo UserInfoFields
err = json.Unmarshal([]byte(user.UserInfo), &userInfo)
if err != nil {
http.Error(rw, "500 Internal Server Error", http.StatusInternalServerError)
return
}
2024-02-07 01:18:17 +00:00
claims := ParseClaims(token.GetScope())
if !claims["openid"] {
http.Error(rw, "Invalid scope", http.StatusBadRequest)
return
}
m := make(map[string]any)
if claims["name"] {
m["name"] = userInfo["name"]
}
if claims["username"] {
m["preferred_username"] = userInfo["preferred_username"]
m["login"] = userInfo["login"]
}
if claims["profile"] {
m["profile"] = userInfo["profile"]
m["picture"] = userInfo["picture"]
m["website"] = userInfo["website"]
}
if claims["email"] {
m["email"] = userInfo["email"]
m["email_verified"] = userInfo["email_verified"]
}
if claims["birthdate"] {
m["birthdate"] = userInfo["birthdate"]
}
if claims["age"] {
m["age"] = userInfo["age"]
}
if claims["zoneinfo"] {
m["zoneinfo"] = userInfo["zoneinfo"]
}
if claims["locale"] {
m["locale"] = userInfo["locale"]
}
2024-02-07 01:18:17 +00:00
m["sub"] = userId
m["aud"] = token.GetClientID()
m["updated_at"] = time.Now().Unix()
_ = json.NewEncoder(rw).Encode(m)
}
r.GET("/userinfo", userInfoRequest)
r.OPTIONS("/userinfo", userInfoRequest)
2024-02-07 01:18:17 +00:00
return &http.Server{
Addr: conf.Listen,
Handler: r,
ReadTimeout: time.Minute,
ReadHeaderTimeout: time.Minute,
WriteTimeout: time.Minute,
IdleTimeout: time.Minute,
MaxHeaderBytes: 2500,
}
2023-11-15 09:21:09 +00:00
}
2023-10-01 21:44:49 +01:00
2024-02-07 01:18:17 +00:00
func (h *HttpServer) SafeRedirect(rw http.ResponseWriter, req *http.Request) {
redirectUrl := req.FormValue("redirect")
if redirectUrl == "" {
http.Redirect(rw, req, "/", http.StatusFound)
return
}
parse, err := url.Parse(redirectUrl)
2023-11-15 09:21:09 +00:00
if err != nil {
2024-02-07 01:18:17 +00:00
http.Error(rw, "Failed to parse redirect url: "+redirectUrl, http.StatusBadRequest)
return
}
if parse.Scheme != "" && parse.Opaque != "" && parse.User != nil && parse.Host != "" {
http.Error(rw, "Invalid redirect url: "+redirectUrl, http.StatusBadRequest)
return
2023-10-01 21:44:49 +01:00
}
2024-02-07 01:18:17 +00:00
http.Redirect(rw, req, parse.String(), http.StatusFound)
}
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
}
2023-11-15 09:21:09 +00:00
2024-02-07 01:18:17 +00:00
a := claims[:n]
claims = claims[n+1:]
if a != "" {
m[a] = true
}
2023-11-15 09:21:09 +00:00
}
2024-02-07 01:18:17 +00:00
return m
2023-10-01 21:44:49 +01:00
}