2023-09-06 22:20:09 +01:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/subtle"
|
|
|
|
_ "embed"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2023-09-09 01:38:10 +01:00
|
|
|
clientStore "github.com/1f349/tulip/client-store"
|
2023-09-06 22:20:09 +01:00
|
|
|
"github.com/1f349/tulip/database"
|
|
|
|
"github.com/1f349/tulip/openid"
|
|
|
|
"github.com/1f349/tulip/pages"
|
|
|
|
"github.com/go-oauth2/oauth2/v4/errors"
|
|
|
|
"github.com/go-oauth2/oauth2/v4/generates"
|
|
|
|
"github.com/go-oauth2/oauth2/v4/manage"
|
|
|
|
"github.com/go-oauth2/oauth2/v4/server"
|
|
|
|
"github.com/go-oauth2/oauth2/v4/store"
|
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
var errMissingRequiredScope = errors.New("missing required scope")
|
|
|
|
|
|
|
|
type HttpServer struct {
|
2023-09-09 01:38:10 +01:00
|
|
|
r *httprouter.Router
|
|
|
|
oauthSrv *server.Server
|
|
|
|
oauthMgr *manage.Manager
|
|
|
|
db *database.DB
|
|
|
|
domain string
|
|
|
|
privKey []byte
|
|
|
|
otpIssuer string
|
2023-09-06 22:20:09 +01:00
|
|
|
}
|
|
|
|
|
2023-09-09 01:38:10 +01: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)
|
|
|
|
if err != nil {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
http.Redirect(rw, req, parse.String(), http.StatusFound)
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewHttpServer(listen, domain, otpIssuer string, db *database.DB, privKey []byte) *http.Server {
|
2023-09-06 22:20:09 +01:00
|
|
|
r := httprouter.New()
|
|
|
|
|
|
|
|
openIdConf := openid.GenConfig(domain, []string{"openid", "email"}, []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 {
|
|
|
|
log.Fatalln("Failed to generate OpenID configuration:", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := pages.LoadPageTemplates(); err != nil {
|
|
|
|
log.Fatalln("Failed to load page templates:", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
oauthManager := manage.NewDefaultManager()
|
|
|
|
oauthSrv := server.NewServer(server.NewConfig(), oauthManager)
|
|
|
|
hs := &HttpServer{
|
2023-09-09 01:38:10 +01:00
|
|
|
r: httprouter.New(),
|
|
|
|
oauthSrv: oauthSrv,
|
|
|
|
oauthMgr: oauthManager,
|
|
|
|
db: db,
|
|
|
|
domain: domain,
|
|
|
|
privKey: privKey,
|
|
|
|
otpIssuer: otpIssuer,
|
2023-09-06 22:20:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
oauthManager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
|
|
|
|
oauthManager.MustTokenStorage(store.NewMemoryTokenStore())
|
|
|
|
oauthManager.MapAccessGenerate(generates.NewAccessGenerate())
|
2023-09-09 01:38:10 +01:00
|
|
|
oauthManager.MapClientStorage(clientStore.New(db))
|
2023-09-06 22:20:09 +01:00
|
|
|
|
|
|
|
oauthSrv.SetResponseErrorHandler(func(re *errors.Response) {
|
|
|
|
log.Printf("Response error: %#v\n", re)
|
|
|
|
})
|
|
|
|
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 a != "openid" {
|
|
|
|
return "", errMissingRequiredScope
|
|
|
|
}
|
|
|
|
return "openid", nil
|
|
|
|
})
|
|
|
|
|
|
|
|
r.GET("/.well-known/openid-configuration", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
|
|
|
rw.WriteHeader(http.StatusOK)
|
|
|
|
_, _ = rw.Write(openIdBytes)
|
|
|
|
})
|
2023-09-09 01:38:10 +01:00
|
|
|
r.GET("/", hs.OptionalAuthentication(false, hs.Home))
|
|
|
|
r.POST("/logout", hs.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
|
2023-09-06 22:20:09 +01:00
|
|
|
lNonce, ok := auth.Session.Get("action-nonce")
|
|
|
|
if !ok {
|
|
|
|
http.Error(rw, "Missing nonce", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if subtle.ConstantTimeCompare([]byte(lNonce.(string)), []byte(req.PostFormValue("nonce"))) == 1 {
|
2023-09-09 01:38:10 +01:00
|
|
|
auth.Session.Delete("session-data")
|
2023-09-06 22:20:09 +01:00
|
|
|
if auth.Session.Save() != nil {
|
|
|
|
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
http.Redirect(rw, req, "/", http.StatusFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
http.Error(rw, "Logout failed", http.StatusInternalServerError)
|
|
|
|
}))
|
2023-09-09 01:38:10 +01:00
|
|
|
r.GET("/login", hs.OptionalAuthentication(false, hs.LoginGet))
|
|
|
|
r.POST("/login", hs.OptionalAuthentication(false, hs.LoginPost))
|
|
|
|
r.GET("/login/otp", hs.OptionalAuthentication(true, hs.LoginOtpGet))
|
|
|
|
r.POST("/login/otp", hs.OptionalAuthentication(true, hs.LoginOtpPost))
|
|
|
|
r.GET("/authorize", hs.RequireAuthentication(hs.authorizeEndpoint))
|
|
|
|
r.POST("/authorize", hs.RequireAuthentication(hs.authorizeEndpoint))
|
2023-09-06 22:20:09 +01:00
|
|
|
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-09-09 01:38:10 +01:00
|
|
|
r.GET("/edit", hs.RequireAuthentication(hs.EditGet))
|
|
|
|
r.POST("/edit", hs.RequireAuthentication(hs.EditPost))
|
|
|
|
r.GET("/edit/otp", hs.RequireAuthentication(hs.EditOtpGet))
|
|
|
|
r.POST("/edit/otp", hs.RequireAuthentication(hs.EditOtpPost))
|
2023-09-06 22:20:09 +01:00
|
|
|
r.GET("/userinfo", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
|
|
|
token, err := oauthSrv.ValidationBearerToken(req)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(rw, "403 Forbidden", http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
fmt.Printf("Using token for user: %s by app: %s with scope: '%s'\n", token.GetUserID(), token.GetClientID(), token.GetScope())
|
|
|
|
_ = json.NewEncoder(rw).Encode(map[string]any{
|
|
|
|
"sub": token.GetUserID(),
|
|
|
|
"aud": token.GetClientID(),
|
|
|
|
"name": "Melon",
|
|
|
|
"preferred_username": "melon",
|
|
|
|
"profile": "https://" + domain + "/user/melon",
|
|
|
|
"picture": "https://" + domain + "/picture/melon.svg",
|
|
|
|
"website": "https://mrmelon54.com",
|
|
|
|
"email": "melon@mrmelon54.com",
|
|
|
|
"email_verified": true,
|
|
|
|
"gender": "male",
|
|
|
|
"birthdate": time.Now().Format(time.DateOnly),
|
|
|
|
"zoneinfo": "Europe/London",
|
|
|
|
"locale": "en-GB",
|
|
|
|
"updated_at": time.Now().Unix(),
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
return &http.Server{
|
|
|
|
Addr: listen,
|
|
|
|
Handler: r,
|
|
|
|
ReadTimeout: time.Minute,
|
|
|
|
ReadHeaderTimeout: time.Minute,
|
|
|
|
WriteTimeout: time.Minute,
|
|
|
|
IdleTimeout: time.Minute,
|
|
|
|
MaxHeaderBytes: 2500,
|
|
|
|
}
|
|
|
|
}
|