Start working on oauth controller

This commit is contained in:
Melon 2024-01-29 23:45:46 +00:00
parent 54f67abffc
commit 0926bf9327
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
49 changed files with 264 additions and 170 deletions

View File

@ -11,8 +11,12 @@ import (
"errors"
"flag"
"github.com/1f349/mjwt"
"github.com/1f349/tulip/purple-server"
"github.com/1f349/tulip/purple-server/pages"
clientStore "github.com/1f349/tulip/client-store"
"github.com/1f349/tulip/cmd/purple-tulip/pages"
"github.com/1f349/tulip/cmd/purple-tulip/server"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/oauth"
"github.com/1f349/tulip/openid"
"github.com/1f349/violet/utils"
exitReload "github.com/MrMelon54/exit-reload"
"github.com/google/subcommands"
@ -38,10 +42,10 @@ func (s *serveCmd) Usage() string {
}
func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
log.Println("[Lavender] Starting...")
log.Println("[PurpleTulip] Starting...")
if s.configPath == "" {
log.Println("[Lavender] Error: config flag is missing")
log.Println("[PurpleTulip] Error: config flag is missing")
return subcommands.ExitUsageError
}
@ -49,45 +53,53 @@ func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
err := loadConfig(s.configPath, &conf)
if err != nil {
if os.IsNotExist(err) {
log.Println("[Lavender] Error: missing config file")
log.Println("[PurpleTulip] Error: missing config file")
} else {
log.Println("[Lavender] Error: loading config file: ", err)
log.Println("[PurpleTulip] Error: loading config file: ", err)
}
return subcommands.ExitFailure
}
configPathAbs, err := filepath.Abs(s.configPath)
if err != nil {
log.Fatal("[Lavender] Failed to get absolute config path")
log.Fatal("[PurpleTulip] Failed to get absolute config path")
}
wd := filepath.Dir(configPathAbs)
mSign, err := mjwt.NewMJwtSignerFromFileOrCreate(conf.Issuer, filepath.Join(wd, "lavender.private.key"), rand.Reader, 4096)
signer, err := mjwt.NewMJwtSignerFromFileOrCreate(conf.Issuer, filepath.Join(wd, "purple-tulip.private.key.pem"), rand.Reader, 4096)
if err != nil {
log.Fatal("[Lavender] Failed to load or create MJWT signer:", err)
log.Fatal("[PurpleTulip] Failed to load or create MJWT signer:", err)
}
saveMjwtPubKey(signer, wd)
db, err := database.Open(filepath.Join(wd, "purple-tulip.db.sqlite"))
if err != nil {
log.Fatal("[PurpleTulip] Failed to open database:", err)
}
saveMjwtPubKey(mSign, wd)
if err := pages.LoadPages(wd); err != nil {
log.Fatal("[Lavender] Failed to load page templates:", err)
log.Fatal("[PurpleTulip] Failed to load page templates:", err)
}
srv := server.NewHttpServer(conf, mSign)
log.Printf("[Lavender] Starting HTTP red-server on '%s'\n", srv.Server.Addr)
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"})
controller := oauth.NewOAuthController(signer, &server.PurpleAuthSource{DB: db}, clientStore.New(db), openIdConf)
srv := server.server.NewHttpServer(conf, db, controller, signer)
log.Printf("[PurpleTulip] Starting HTTP server on '%s'\n", srv.Server.Addr)
go utils.RunBackgroundHttp("HTTP", srv.Server)
exitReload.ExitReload("Lavender", func() {
exitReload.ExitReload("PurpleTulip", func() {
var conf server.Conf
err := loadConfig(s.configPath, &conf)
if err != nil {
log.Println("[Lavender] Failed to read config:", err)
log.Println("[PurpleTulip] Failed to read config:", err)
}
err = srv.UpdateConfig(conf)
if err != nil {
log.Println("[Lavender] Failed to reload config:", err)
log.Println("[PurpleTulip] Failed to reload config:", err)
}
}, func() {
// stop http red-server
// stop http server
_ = srv.Server.Close()
})
@ -108,10 +120,10 @@ func saveMjwtPubKey(mSign mjwt.Signer, wd string) {
b := new(bytes.Buffer)
err := pem.Encode(b, &pem.Block{Type: "RSA PUBLIC KEY", Bytes: pubKey})
if err != nil {
log.Fatal("[Lavender] Failed to encode MJWT public key:", err)
log.Fatal("[PurpleTulip] Failed to encode MJWT public key:", err)
}
err = os.WriteFile(filepath.Join(wd, "lavender.public.key"), b.Bytes(), 0600)
if err != nil && !errors.Is(err, os.ErrExist) {
log.Fatal("[Lavender] Failed to save MJWT public key:", err)
log.Fatal("[PurpleTulip] Failed to save MJWT public key:", err)
}
}

View File

@ -4,8 +4,8 @@ import (
"context"
_ "embed"
"fmt"
"github.com/1f349/tulip/cmd/purple-tulip/pages"
"github.com/1f349/tulip/issuer"
"github.com/1f349/tulip/purple-server/pages"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
"golang.org/x/oauth2"

View File

@ -9,8 +9,8 @@ import (
"fmt"
"github.com/1f349/cache"
"github.com/1f349/mjwt"
"github.com/1f349/tulip/cmd/purple-tulip/pages"
"github.com/1f349/tulip/issuer"
"github.com/1f349/tulip/purple-server/pages"
"github.com/1f349/tulip/utils"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
@ -72,7 +72,7 @@ var testHttpServer = HttpServer{
func init() {
testHttpServer.conf.Store(&Conf{
BaseUrl: lavenderDomain,
ServiceName: "Test Lavender Service",
ServiceName: "Test Purple Tulip Service",
})
testHttpServer.manager.Store(testManager)
testHttpServer.services.Store(&map[string]AllowedClient{
@ -353,7 +353,7 @@ func TestFlowCallback(t *testing.T) {
const p1 = `<!DOCTYPE html>
<html lang="en">
<head>
<title>Test Lavender Service</title>
<title>Test Purple Tulip Service</title>
<link rel="stylesheet" href="/theme/style.css">
<script>
let loginData = {
@ -372,7 +372,7 @@ func TestFlowCallback(t *testing.T) {
</head>
<body>
<header>
<h1>Test Lavender Service</h1>
<h1>Test Purple Tulip Service</h1>
</header>
<main id="mainBody">Loading...</main>
</body>

View File

@ -86,13 +86,13 @@ func (h *HttpServer) finishTokenGenerateFlow(rw http.ResponseWriter, req *http.R
marshal, err := json.Marshal(exchange)
if err != nil {
fmt.Println("Failed to marshal exchange tokens", err)
http.Error(rw, "Internal red-server error", http.StatusInternalServerError)
http.Error(rw, "Internal server error", http.StatusInternalServerError)
return
}
oaepBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, h.signer.PublicKey(), marshal, []byte("sso-exchange"))
if err != nil {
fmt.Println("Failed to encrypt exchange tokens", err)
http.Error(rw, "Internal red-server error", http.StatusInternalServerError)
http.Error(rw, "Internal server error", http.StatusInternalServerError)
return
}
http.SetCookie(rw, &http.Cookie{

View File

@ -5,7 +5,9 @@ import (
"fmt"
"github.com/1f349/cache"
"github.com/1f349/mjwt"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/issuer"
"github.com/1f349/tulip/oauth"
"github.com/1f349/tulip/theme"
"github.com/julienschmidt/httprouter"
"github.com/rs/cors"
@ -31,7 +33,7 @@ type flowStateData struct {
target AllowedClient
}
func NewHttpServer(conf Conf, signer mjwt.Signer) *HttpServer {
func NewHttpServer(conf Conf, db *database.DB, controller *oauth.Controller, signer mjwt.Signer) *HttpServer {
r := httprouter.New()
// remove last slash from baseUrl

View File

@ -1,4 +1,4 @@
package red_pages
package pages
import (
"embed"
@ -33,7 +33,7 @@ func LoadPages(wd string) (err error) {
wdFs := os.DirFS(wwwDir)
o = overlapfs.OverlapFS{A: wwwPages, B: wdFs}
}
wwwTemplates, err = template.New("red-pages").Funcs(template.FuncMap{
wwwTemplates, err = template.New("pages").Funcs(template.FuncMap{
"emailHide": EmailHide,
}).ParseFS(o, "*.go.html")
})

View File

@ -1,4 +1,4 @@
package red_pages
package pages
import (
"github.com/stretchr/testify/assert"

View File

@ -9,10 +9,13 @@ import (
"flag"
"fmt"
"github.com/1f349/mjwt"
clientStore "github.com/1f349/tulip/client-store"
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/cmd/red-tulip/server"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/mail/templates"
"github.com/1f349/tulip/red-pages"
"github.com/1f349/tulip/red-server"
"github.com/1f349/tulip/oauth"
"github.com/1f349/tulip/openid"
"github.com/1f349/violet/utils"
"github.com/MrMelon54/exit-reload"
"github.com/google/subcommands"
@ -46,39 +49,29 @@ func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...any) subcomm
return subcommands.ExitUsageError
}
openConf, err := os.Open(s.configPath)
var conf server.Conf
err := loadConfig(s.configPath, &conf)
if err != nil {
if os.IsNotExist(err) {
log.Println("[RedTulip] Error: missing config file")
} else {
log.Println("[RedTulip] Error: open config file: ", err)
log.Println("[RedTulip] Error: loading config file: ", err)
}
return subcommands.ExitFailure
}
var config red_server.Conf
err = json.NewDecoder(openConf).Decode(&config)
if err != nil {
log.Println("[RedTulip] Error: invalid config file: ", err)
return subcommands.ExitFailure
}
configPathAbs, err := filepath.Abs(s.configPath)
if err != nil {
log.Fatal("[RedTulip] Failed to get absolute config path")
}
wd := filepath.Dir(configPathAbs)
normalLoad(config, wd)
return subcommands.ExitSuccess
}
func normalLoad(startUp red_server.Conf, wd string) {
signingKey, err := mjwt.NewMJwtSignerFromFileOrCreate(startUp.OtpIssuer, filepath.Join(wd, "tulip.key.pem"), rand.Reader, 4096)
signer, err := mjwt.NewMJwtSignerFromFileOrCreate(conf.OtpIssuer, filepath.Join(wd, "red-tulip.key.pem"), rand.Reader, 4096)
if err != nil {
log.Fatal("[Tulip] Failed to open signing key file:", err)
}
db, err := database.Open(filepath.Join(wd, "red-red-tulip.db.sqlite"))
db, err := database.Open(filepath.Join(wd, "red-tulip.db.sqlite"))
if err != nil {
log.Fatal("[RedTulip] Failed to open database:", err)
}
@ -88,34 +81,36 @@ func normalLoad(startUp red_server.Conf, wd string) {
log.Fatal("[RedTulip] Failed check:", err)
}
if err = red_pages.LoadPages(wd); err != nil {
if err = pages.LoadPages(wd); err != nil {
log.Fatal("[RedTulip] Failed to load page templates:", err)
}
if err := templates.LoadMailTemplates(wd); err != nil {
log.Fatal("[RedTulip] Failed to load mail templates:", err)
}
srv := red_server.NewHttpServer(startUp, db, signingKey)
log.Printf("[RedTulip] Starting HTTP red-server on '%s'\n", srv.Addr)
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"})
controller := oauth.NewOAuthController(signer, &server.RedAuthSource{DB: db}, clientStore.New(db), openIdConf)
srv := server.NewHttpServer(conf, db, controller, signer)
log.Printf("[RedTulip] Starting HTTP server on '%s'\n", srv.Addr)
go utils.RunBackgroundHttp("HTTP", srv)
exit_reload.ExitReload("RedTulip", func() {}, func() {
// stop http red-server
// stop http server
_ = srv.Close()
_ = db.Close()
})
return subcommands.ExitSuccess
}
func genHmacKey() []byte {
a := make([]byte, 32)
n, err := rand.Reader.Read(a)
func loadConfig(configPath string, conf *server.Conf) error {
openConf, err := os.Open(configPath)
if err != nil {
log.Fatal("[RedTulip] Failed to generate HMAC key")
return err
}
if n != 32 {
log.Fatal("[RedTulip] Failed to generate HMAC key")
}
return a
return json.NewDecoder(openConf).Decode(conf)
}
func checkDbHasUser(db *database.DB) error {

View File

@ -0,0 +1,45 @@
package server
import (
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/oauth"
"net/http"
"net/url"
)
type RedAuthSource struct {
DB *database.DB
}
var _ oauth.AuthSource = &RedAuthSource{}
func (r *RedAuthSource) UserAuthorization(rw http.ResponseWriter, req *http.Request) (string, error) {
err := req.ParseForm()
if err != nil {
return "", err
}
auth, err := internalAuthenticationHandler(rw, req)
if err != nil {
return "", err
}
if auth.IsGuest() {
// handle redirecting to oauth
var q url.Values
switch req.Method {
case http.MethodPost:
q = req.PostForm
case http.MethodGet:
q = req.URL.Query()
default:
http.Error(rw, "405 Method Not Allowed", http.StatusMethodNotAllowed)
return "", err
}
redirectUrl := PrepareRedirectUrl("/login", &url.URL{Path: "/authorize", RawQuery: q.Encode()})
http.Redirect(rw, req, redirectUrl.String(), http.StatusFound)
return "", nil
}
return auth.Data.ID.String(), nil
}

View File

@ -1,4 +1,4 @@
package red_server
package server
import (
"crypto/rand"

View File

@ -1,4 +1,4 @@
package red_server
package server
import (
"context"

View File

@ -1,4 +1,4 @@
package red_server
package server
import "github.com/1f349/tulip/mail"

View File

@ -1,4 +1,4 @@
package red_server
package server
import (
"github.com/1f349/tulip/database"

View File

@ -1,9 +1,9 @@
package red_server
package server
import (
"fmt"
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/1f349/tulip/utils"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
@ -30,7 +30,7 @@ func (h *HttpServer) EditGet(rw http.ResponseWriter, _ *http.Request, _ httprout
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
return
}
red_pages.RenderPageTemplate(rw, "edit", map[string]any{
pages.RenderPageTemplate(rw, "edit", map[string]any{
"ServiceName": h.conf.ServiceName,
"User": user,
"Nonce": lNonce,

View File

@ -1,9 +1,9 @@
package red_server
package server
import (
"fmt"
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
"net/http"
@ -13,7 +13,7 @@ func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httproute
rw.Header().Set("Content-Type", "text/html")
rw.WriteHeader(http.StatusOK)
if auth.IsGuest() {
red_pages.RenderPageTemplate(rw, "index-guest", map[string]any{
pages.RenderPageTemplate(rw, "index-guest", map[string]any{
"ServiceName": h.conf.ServiceName,
})
return
@ -41,7 +41,7 @@ func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httproute
}) {
return
}
red_pages.RenderPageTemplate(rw, "index", map[string]any{
pages.RenderPageTemplate(rw, "index", map[string]any{
"ServiceName": h.conf.ServiceName,
"Auth": auth,
"User": userWithName,

View File

@ -1,4 +1,4 @@
package red_server
package server
import (
"crypto/rand"
@ -8,8 +8,8 @@ import (
"encoding/base64"
"errors"
"fmt"
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/emersion/go-message/mail"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
@ -46,7 +46,7 @@ func (h *HttpServer) LoginGet(rw http.ResponseWriter, req *http.Request, _ httpr
rw.Header().Set("Content-Type", "text/html")
rw.WriteHeader(http.StatusOK)
red_pages.RenderPageTemplate(rw, "login", map[string]any{
pages.RenderPageTemplate(rw, "login", map[string]any{
"ServiceName": h.conf.ServiceName,
"Redirect": req.URL.Query().Get("redirect"),
"Mismatch": req.URL.Query().Get("mismatch"),
@ -70,7 +70,7 @@ func (h *HttpServer) LoginPost(rw http.ResponseWriter, req *http.Request, _ http
loginMismatch = 1
return nil
}
http.Error(rw, "Internal red-server error", http.StatusInternalServerError)
http.Error(rw, "Internal server error", http.StatusInternalServerError)
return err
}

View File

@ -1,8 +1,8 @@
package red_server
package server
import (
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/emersion/go-message/mail"
"github.com/go-session/session"
"github.com/google/uuid"
@ -67,7 +67,7 @@ func (h *HttpServer) MailPassword(rw http.ResponseWriter, req *http.Request, par
return
}
red_pages.RenderPageTemplate(rw, "reset-password", map[string]any{
pages.RenderPageTemplate(rw, "reset-password", map[string]any{
"ServiceName": h.conf.ServiceName,
})
}

View File

@ -1,8 +1,8 @@
package red_server
package server
import (
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/go-oauth2/oauth2/v4"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
@ -58,7 +58,7 @@ func (h *HttpServer) ManageAppsGet(rw http.ResponseWriter, req *http.Request, _
validEdit:
rw.Header().Set("Content-Type", "text/html")
rw.WriteHeader(http.StatusOK)
red_pages.RenderPageTemplate(rw, "manage-apps", m)
pages.RenderPageTemplate(rw, "manage-apps", m)
}
func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {

View File

@ -1,9 +1,9 @@
package red_server
package server
import (
"errors"
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/emersion/go-message/mail"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
@ -66,7 +66,7 @@ func (h *HttpServer) ManageUsersGet(rw http.ResponseWriter, req *http.Request, _
validEdit:
rw.Header().Set("Content-Type", "text/html")
rw.WriteHeader(http.StatusOK)
red_pages.RenderPageTemplate(rw, "manage-users", m)
pages.RenderPageTemplate(rw, "manage-users", m)
}
func (h *HttpServer) ManageUsersPost(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {

View File

@ -1,8 +1,8 @@
package red_server
package server
import (
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/1f349/tulip/utils"
"github.com/julienschmidt/httprouter"
"net/http"
@ -100,7 +100,7 @@ func (h *HttpServer) authorizeEndpoint(rw http.ResponseWriter, req *http.Request
}
rw.WriteHeader(http.StatusOK)
red_pages.RenderPageTemplate(rw, "oauth-authorize", map[string]any{
pages.RenderPageTemplate(rw, "oauth-authorize", map[string]any{
"ServiceName": h.conf.ServiceName,
"AppName": appName,
"AppDomain": appDomain,
@ -143,34 +143,3 @@ func (h *HttpServer) authorizeEndpoint(rw http.ResponseWriter, req *http.Request
parsedRedirect.RawQuery = q.Encode()
http.Redirect(rw, req, parsedRedirect.String(), http.StatusFound)
}
func (h *HttpServer) oauthUserAuthorization(rw http.ResponseWriter, req *http.Request) (string, error) {
err := req.ParseForm()
if err != nil {
return "", err
}
auth, err := internalAuthenticationHandler(rw, req)
if err != nil {
return "", err
}
if auth.IsGuest() {
// handle redirecting to oauth
var q url.Values
switch req.Method {
case http.MethodPost:
q = req.PostForm
case http.MethodGet:
q = req.URL.Query()
default:
http.Error(rw, "405 Method Not Allowed", http.StatusMethodNotAllowed)
return "", err
}
redirectUrl := PrepareRedirectUrl("/login", &url.URL{Path: "/authorize", RawQuery: q.Encode()})
http.Redirect(rw, req, redirectUrl.String(), http.StatusFound)
return "", nil
}
return auth.Data.ID.String(), nil
}

View File

@ -1,10 +1,10 @@
package red_server
package server
import (
"bytes"
"encoding/base64"
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
"github.com/skip2/go-qrcode"
@ -21,7 +21,7 @@ func (h *HttpServer) LoginOtpGet(rw http.ResponseWriter, req *http.Request, _ ht
return
}
red_pages.RenderPageTemplate(rw, "login-otp", map[string]any{
pages.RenderPageTemplate(rw, "login-otp", map[string]any{
"ServiceName": h.conf.ServiceName,
"Redirect": req.URL.Query().Get("redirect"),
})
@ -80,7 +80,7 @@ func (h *HttpServer) EditOtpPost(rw http.ResponseWriter, req *http.Request, _ ht
if req.Method == http.MethodPost && req.FormValue("remove") == "1" {
if !req.Form.Has("code") {
// render page
red_pages.RenderPageTemplate(rw, "remove-otp", map[string]any{
pages.RenderPageTemplate(rw, "remove-otp", map[string]any{
"ServiceName": h.conf.ServiceName,
})
return
@ -154,7 +154,7 @@ func (h *HttpServer) EditOtpPost(rw http.ResponseWriter, req *http.Request, _ ht
}
// render page
red_pages.RenderPageTemplate(rw, "edit-otp", map[string]any{
pages.RenderPageTemplate(rw, "edit-otp", map[string]any{
"ServiceName": h.conf.ServiceName,
"OtpQr": template.URL("data:qrImg/png;base64," + base64.StdEncoding.EncodeToString(qrBuf.Bytes())),
"QrWidth": qrWidth,

View File

@ -1,4 +1,4 @@
package red_server
package server
import (
"bytes"
@ -8,16 +8,12 @@ import (
"fmt"
"github.com/1f349/cache"
"github.com/1f349/mjwt"
clientStore "github.com/1f349/tulip/client-store"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/openid"
"github.com/1f349/tulip/oauth"
"github.com/1f349/tulip/theme"
scope2 "github.com/1f349/tulip/utils"
"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/google/uuid"
"github.com/julienschmidt/httprouter"
"log"
@ -52,7 +48,7 @@ type mailLinkKey struct {
data uuid.UUID
}
func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Server {
func NewHttpServer(conf Conf, db *database.DB, oauthController *oauth.Controller, signingKey mjwt.Signer) *http.Server {
r := httprouter.New()
// remove last slash from baseUrl
@ -63,8 +59,7 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
}
}
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)
openIdBytes, err := json.Marshal(oauthController.OidConf)
if err != nil {
log.Fatalln("Failed to generate OpenID configuration:", err)
}
@ -82,39 +77,6 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
mailLinkCache: cache.New[mailLinkKey, uuid.UUID](),
}
oauthManager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
oauthManager.MustTokenStorage(store.NewMemoryTokenStore())
oauthManager.MapAccessGenerate(generates.NewAccessGenerate())
oauthManager.MapClientStorage(clientStore.New(db))
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 !scope2.ScopesExist(a) {
return "", errInvalidScope
}
return a, nil
})
r.GET("/.well-known/openid-configuration", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write(openIdBytes)
@ -164,18 +126,18 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
r.POST("/mail/password", hs.MailPasswordPost)
r.GET("/mail/delete/:code", hs.MailDelete)
// edit profile red-pages
// edit profile pages
r.GET("/edit", hs.RequireAuthentication(hs.EditGet))
r.POST("/edit", hs.RequireAuthentication(hs.EditPost))
r.POST("/edit/otp", hs.RequireAuthentication(hs.EditOtpPost))
// management red-pages
// management pages
r.GET("/manage/apps", hs.RequireAdminAuthentication(hs.ManageAppsGet))
r.POST("/manage/apps", hs.RequireAdminAuthentication(hs.ManageAppsPost))
r.GET("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersGet))
r.POST("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersPost))
// oauth red-pages
// 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) {

View File

@ -1,4 +1,4 @@
package red_server
package server
import (
"fmt"

View File

@ -13,7 +13,7 @@ import (
type Mail struct {
Name string `json:"name"`
Tls bool `json:"tls"`
Server string `json:"red-server"`
Server string `json:"server"`
From FromAddress `json:"from"`
Username string `json:"username"`
Password string `json:"password"`

74
oauth/controller.go Normal file
View File

@ -0,0 +1,74 @@
package oauth
import (
"github.com/1f349/mjwt"
"github.com/1f349/tulip/openid"
scope2 "github.com/1f349/tulip/utils"
"github.com/go-oauth2/oauth2/v4"
"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"
"log"
"net/http"
"net/url"
"strings"
)
var errInvalidScope = errors.New("missing required scope")
type Controller struct {
baseUrl string
mgr *manage.Manager
srv *server.Server
OidConf openid.Config
}
type AuthSource interface {
UserAuthorization(rw http.ResponseWriter, req *http.Request) (string, error)
}
func NewOAuthController(signer mjwt.Signer, source AuthSource, clientStore oauth2.ClientStore, oidConf openid.Config) *Controller {
c := &Controller{
// remove last slash from baseUrl
baseUrl: strings.TrimSuffix(oidConf.Issuer, "/"),
mgr: manage.NewDefaultManager(),
OidConf: oidConf,
}
c.srv = server.NewServer(server.NewConfig(), c.mgr)
c.mgr.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
c.mgr.MustTokenStorage(store.NewMemoryTokenStore())
c.mgr.MapAccessGenerate(NewJWTAccessGenerate(signer))
c.mgr.MapClientStorage(clientStore)
c.srv.SetResponseErrorHandler(func(re *errors.Response) {
log.Printf("Response error: %#v\n", re)
})
c.srv.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
})
c.srv.SetUserAuthorizationHandler(source.UserAuthorization)
c.srv.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
})
return c
}

35
oauth/jwt.go Normal file
View File

@ -0,0 +1,35 @@
package oauth
import (
"context"
"crypto/sha256"
"encoding/base64"
"github.com/1f349/mjwt"
"github.com/1f349/mjwt/auth"
"github.com/go-oauth2/oauth2/v4"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"strings"
)
type JWTAccessGenerate struct {
signer mjwt.Signer
}
func NewJWTAccessGenerate(signer mjwt.Signer) *JWTAccessGenerate {
return &JWTAccessGenerate{signer}
}
var _ oauth2.AccessGenerate = &JWTAccessGenerate{}
func (j JWTAccessGenerate) Token(ctx context.Context, data *oauth2.GenerateBasic, isGenRefresh bool) (access, refresh string, err error) {
access, err = j.signer.GenerateJwt(data.UserID, "", jwt.ClaimStrings{data.Client.GetID()}, data.TokenInfo.GetAccessExpiresIn(), auth.AccessTokenClaims{})
if isGenRefresh {
t := uuid.NewHash(sha256.New(), uuid.New(), []byte(access), 5).String()
refresh = base64.URLEncoding.EncodeToString([]byte(t))
refresh = strings.ToUpper(strings.TrimRight(refresh, "="))
}
return
}

View File

@ -84,7 +84,7 @@
function doThisThing() {
if (currentLoginPopup) currentLoginPopup.close();
currentLoginPopup = popupCenterScreen(ssoService + '/popup?origin=' + encodeURIComponent(location.origin), 'Login with Lavender', 500, 500, false);
currentLoginPopup = popupCenterScreen(ssoService + '/popup?origin=' + encodeURIComponent(location.origin), 'Login with Purple Tulip', 500, 500, false);
}
async function refreshAllTokens() {

BIN
purple-tulip Executable file

Binary file not shown.

BIN
red-tulip Executable file

Binary file not shown.