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" "errors"
"flag" "flag"
"github.com/1f349/mjwt" "github.com/1f349/mjwt"
"github.com/1f349/tulip/purple-server" clientStore "github.com/1f349/tulip/client-store"
"github.com/1f349/tulip/purple-server/pages" "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" "github.com/1f349/violet/utils"
exitReload "github.com/MrMelon54/exit-reload" exitReload "github.com/MrMelon54/exit-reload"
"github.com/google/subcommands" "github.com/google/subcommands"
@ -38,10 +42,10 @@ func (s *serveCmd) Usage() string {
} }
func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
log.Println("[Lavender] Starting...") log.Println("[PurpleTulip] Starting...")
if s.configPath == "" { if s.configPath == "" {
log.Println("[Lavender] Error: config flag is missing") log.Println("[PurpleTulip] Error: config flag is missing")
return subcommands.ExitUsageError return subcommands.ExitUsageError
} }
@ -49,45 +53,53 @@ func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
err := loadConfig(s.configPath, &conf) err := loadConfig(s.configPath, &conf)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
log.Println("[Lavender] Error: missing config file") log.Println("[PurpleTulip] Error: missing config file")
} else { } else {
log.Println("[Lavender] Error: loading config file: ", err) log.Println("[PurpleTulip] Error: loading config file: ", err)
} }
return subcommands.ExitFailure return subcommands.ExitFailure
} }
configPathAbs, err := filepath.Abs(s.configPath) configPathAbs, err := filepath.Abs(s.configPath)
if err != nil { 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) 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 { 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 { 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) 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"})
log.Printf("[Lavender] Starting HTTP red-server on '%s'\n", srv.Server.Addr) 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) go utils.RunBackgroundHttp("HTTP", srv.Server)
exitReload.ExitReload("Lavender", func() { exitReload.ExitReload("PurpleTulip", func() {
var conf server.Conf var conf server.Conf
err := loadConfig(s.configPath, &conf) err := loadConfig(s.configPath, &conf)
if err != nil { if err != nil {
log.Println("[Lavender] Failed to read config:", err) log.Println("[PurpleTulip] Failed to read config:", err)
} }
err = srv.UpdateConfig(conf) err = srv.UpdateConfig(conf)
if err != nil { if err != nil {
log.Println("[Lavender] Failed to reload config:", err) log.Println("[PurpleTulip] Failed to reload config:", err)
} }
}, func() { }, func() {
// stop http red-server // stop http server
_ = srv.Server.Close() _ = srv.Server.Close()
}) })
@ -108,10 +120,10 @@ func saveMjwtPubKey(mSign mjwt.Signer, wd string) {
b := new(bytes.Buffer) b := new(bytes.Buffer)
err := pem.Encode(b, &pem.Block{Type: "RSA PUBLIC KEY", Bytes: pubKey}) err := pem.Encode(b, &pem.Block{Type: "RSA PUBLIC KEY", Bytes: pubKey})
if err != nil { 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) err = os.WriteFile(filepath.Join(wd, "lavender.public.key"), b.Bytes(), 0600)
if err != nil && !errors.Is(err, os.ErrExist) { 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" "context"
_ "embed" _ "embed"
"fmt" "fmt"
"github.com/1f349/tulip/cmd/purple-tulip/pages"
"github.com/1f349/tulip/issuer" "github.com/1f349/tulip/issuer"
"github.com/1f349/tulip/purple-server/pages"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"golang.org/x/oauth2" "golang.org/x/oauth2"

View File

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

View File

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

View File

@ -5,7 +5,9 @@ import (
"fmt" "fmt"
"github.com/1f349/cache" "github.com/1f349/cache"
"github.com/1f349/mjwt" "github.com/1f349/mjwt"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/issuer" "github.com/1f349/tulip/issuer"
"github.com/1f349/tulip/oauth"
"github.com/1f349/tulip/theme" "github.com/1f349/tulip/theme"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/rs/cors" "github.com/rs/cors"
@ -31,7 +33,7 @@ type flowStateData struct {
target AllowedClient 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() r := httprouter.New()
// remove last slash from baseUrl // remove last slash from baseUrl

View File

@ -1,4 +1,4 @@
package red_pages package pages
import ( import (
"embed" "embed"
@ -33,7 +33,7 @@ func LoadPages(wd string) (err error) {
wdFs := os.DirFS(wwwDir) wdFs := os.DirFS(wwwDir)
o = overlapfs.OverlapFS{A: wwwPages, B: wdFs} 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, "emailHide": EmailHide,
}).ParseFS(o, "*.go.html") }).ParseFS(o, "*.go.html")
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
package red_server package server
import ( import (
"fmt" "fmt"
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database" "github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/1f349/tulip/utils" "github.com/1f349/tulip/utils"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/julienschmidt/httprouter" "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) http.Error(rw, "Failed to save session", http.StatusInternalServerError)
return return
} }
red_pages.RenderPageTemplate(rw, "edit", map[string]any{ pages.RenderPageTemplate(rw, "edit", map[string]any{
"ServiceName": h.conf.ServiceName, "ServiceName": h.conf.ServiceName,
"User": user, "User": user,
"Nonce": lNonce, "Nonce": lNonce,

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
package red_server package server
import ( import (
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database" "github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/go-oauth2/oauth2/v4" "github.com/go-oauth2/oauth2/v4"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
@ -58,7 +58,7 @@ func (h *HttpServer) ManageAppsGet(rw http.ResponseWriter, req *http.Request, _
validEdit: validEdit:
rw.Header().Set("Content-Type", "text/html") rw.Header().Set("Content-Type", "text/html")
rw.WriteHeader(http.StatusOK) 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) { 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 ( import (
"errors" "errors"
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database" "github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/emersion/go-message/mail" "github.com/emersion/go-message/mail"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
@ -66,7 +66,7 @@ func (h *HttpServer) ManageUsersGet(rw http.ResponseWriter, req *http.Request, _
validEdit: validEdit:
rw.Header().Set("Content-Type", "text/html") rw.Header().Set("Content-Type", "text/html")
rw.WriteHeader(http.StatusOK) 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) { 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 ( import (
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database" "github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/1f349/tulip/utils" "github.com/1f349/tulip/utils"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"net/http" "net/http"
@ -100,7 +100,7 @@ func (h *HttpServer) authorizeEndpoint(rw http.ResponseWriter, req *http.Request
} }
rw.WriteHeader(http.StatusOK) 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, "ServiceName": h.conf.ServiceName,
"AppName": appName, "AppName": appName,
"AppDomain": appDomain, "AppDomain": appDomain,
@ -143,34 +143,3 @@ func (h *HttpServer) authorizeEndpoint(rw http.ResponseWriter, req *http.Request
parsedRedirect.RawQuery = q.Encode() parsedRedirect.RawQuery = q.Encode()
http.Redirect(rw, req, parsedRedirect.String(), http.StatusFound) 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 ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"github.com/1f349/tulip/cmd/red-tulip/pages"
"github.com/1f349/tulip/database" "github.com/1f349/tulip/database"
"github.com/1f349/tulip/red-pages"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/skip2/go-qrcode" "github.com/skip2/go-qrcode"
@ -21,7 +21,7 @@ func (h *HttpServer) LoginOtpGet(rw http.ResponseWriter, req *http.Request, _ ht
return return
} }
red_pages.RenderPageTemplate(rw, "login-otp", map[string]any{ pages.RenderPageTemplate(rw, "login-otp", map[string]any{
"ServiceName": h.conf.ServiceName, "ServiceName": h.conf.ServiceName,
"Redirect": req.URL.Query().Get("redirect"), "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.Method == http.MethodPost && req.FormValue("remove") == "1" {
if !req.Form.Has("code") { if !req.Form.Has("code") {
// render page // render page
red_pages.RenderPageTemplate(rw, "remove-otp", map[string]any{ pages.RenderPageTemplate(rw, "remove-otp", map[string]any{
"ServiceName": h.conf.ServiceName, "ServiceName": h.conf.ServiceName,
}) })
return return
@ -154,7 +154,7 @@ func (h *HttpServer) EditOtpPost(rw http.ResponseWriter, req *http.Request, _ ht
} }
// render page // render page
red_pages.RenderPageTemplate(rw, "edit-otp", map[string]any{ pages.RenderPageTemplate(rw, "edit-otp", map[string]any{
"ServiceName": h.conf.ServiceName, "ServiceName": h.conf.ServiceName,
"OtpQr": template.URL("data:qrImg/png;base64," + base64.StdEncoding.EncodeToString(qrBuf.Bytes())), "OtpQr": template.URL("data:qrImg/png;base64," + base64.StdEncoding.EncodeToString(qrBuf.Bytes())),
"QrWidth": qrWidth, "QrWidth": qrWidth,

View File

@ -1,4 +1,4 @@
package red_server package server
import ( import (
"bytes" "bytes"
@ -8,16 +8,12 @@ import (
"fmt" "fmt"
"github.com/1f349/cache" "github.com/1f349/cache"
"github.com/1f349/mjwt" "github.com/1f349/mjwt"
clientStore "github.com/1f349/tulip/client-store"
"github.com/1f349/tulip/database" "github.com/1f349/tulip/database"
"github.com/1f349/tulip/openid" "github.com/1f349/tulip/oauth"
"github.com/1f349/tulip/theme" "github.com/1f349/tulip/theme"
scope2 "github.com/1f349/tulip/utils"
"github.com/go-oauth2/oauth2/v4/errors" "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/manage"
"github.com/go-oauth2/oauth2/v4/server" "github.com/go-oauth2/oauth2/v4/server"
"github.com/go-oauth2/oauth2/v4/store"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"log" "log"
@ -52,7 +48,7 @@ type mailLinkKey struct {
data uuid.UUID 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() r := httprouter.New()
// remove last slash from baseUrl // 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(oauthController.OidConf)
openIdBytes, err := json.Marshal(openIdConf)
if err != nil { if err != nil {
log.Fatalln("Failed to generate OpenID configuration:", err) 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](), 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) { r.GET("/.well-known/openid-configuration", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
_, _ = rw.Write(openIdBytes) _, _ = 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.POST("/mail/password", hs.MailPasswordPost)
r.GET("/mail/delete/:code", hs.MailDelete) r.GET("/mail/delete/:code", hs.MailDelete)
// edit profile red-pages // edit profile pages
r.GET("/edit", hs.RequireAuthentication(hs.EditGet)) r.GET("/edit", hs.RequireAuthentication(hs.EditGet))
r.POST("/edit", hs.RequireAuthentication(hs.EditPost)) r.POST("/edit", hs.RequireAuthentication(hs.EditPost))
r.POST("/edit/otp", hs.RequireAuthentication(hs.EditOtpPost)) r.POST("/edit/otp", hs.RequireAuthentication(hs.EditOtpPost))
// management red-pages // management pages
r.GET("/manage/apps", hs.RequireAdminAuthentication(hs.ManageAppsGet)) r.GET("/manage/apps", hs.RequireAdminAuthentication(hs.ManageAppsGet))
r.POST("/manage/apps", hs.RequireAdminAuthentication(hs.ManageAppsPost)) r.POST("/manage/apps", hs.RequireAdminAuthentication(hs.ManageAppsPost))
r.GET("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersGet)) r.GET("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersGet))
r.POST("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersPost)) r.POST("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersPost))
// oauth red-pages // oauth pages
r.GET("/authorize", hs.RequireAuthentication(hs.authorizeEndpoint)) r.GET("/authorize", hs.RequireAuthentication(hs.authorizeEndpoint))
r.POST("/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) { 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 ( import (
"fmt" "fmt"

View File

@ -13,7 +13,7 @@ import (
type Mail struct { type Mail struct {
Name string `json:"name"` Name string `json:"name"`
Tls bool `json:"tls"` Tls bool `json:"tls"`
Server string `json:"red-server"` Server string `json:"server"`
From FromAddress `json:"from"` From FromAddress `json:"from"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` 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() { function doThisThing() {
if (currentLoginPopup) currentLoginPopup.close(); 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() { async function refreshAllTokens() {

BIN
purple-tulip Executable file

Binary file not shown.

BIN
red-tulip Executable file

Binary file not shown.