mirror of
https://github.com/1f349/tulip.git
synced 2024-12-22 16:24:10 +00:00
Start working on oauth controller
This commit is contained in:
parent
54f67abffc
commit
0926bf9327
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
@ -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>
|
@ -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{
|
@ -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
|
@ -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")
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
package red_pages
|
||||
package pages
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
@ -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 {
|
||||
|
45
cmd/red-tulip/server/auth-source.go
Normal file
45
cmd/red-tulip/server/auth-source.go
Normal 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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package red_server
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
@ -1,4 +1,4 @@
|
||||
package red_server
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,4 +1,4 @@
|
||||
package red_server
|
||||
package server
|
||||
|
||||
import "github.com/1f349/tulip/mail"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package red_server
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/1f349/tulip/database"
|
@ -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,
|
@ -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,
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
@ -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) {
|
@ -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) {
|
@ -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
|
||||
}
|
@ -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,
|
@ -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) {
|
@ -1,4 +1,4 @@
|
||||
package red_server
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -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
74
oauth/controller.go
Normal 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
35
oauth/jwt.go
Normal 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
|
||||
}
|
@ -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
BIN
purple-tulip
Executable file
Binary file not shown.
Loading…
Reference in New Issue
Block a user