From 5607bd3a970b56e1b37cd70c7212a85279897acf Mon Sep 17 00:00:00 2001 From: MrMelon54 Date: Mon, 9 Dec 2024 18:40:18 +0000 Subject: [PATCH] Rewrite login system --- auth/auth.go | 8 +- auth/providers/login.go | 12 +- auth/providers/oauth.go | 8 +- auth/providers/otp.go | 16 +-- auth/providers/passkey.go | 15 +-- auth/userauth.go | 2 +- server/auth_test.go | 2 +- server/login.go | 17 +-- web/src/layouts/Layout.astro | 14 ++- web/src/pages/{edit.go.html => edit.astro} | 9 ++ web/src/pages/index.astro | 132 +++------------------ web/web.go | 11 +- 12 files changed, 87 insertions(+), 159 deletions(-) rename web/src/pages/{edit.go.html => edit.astro} (97%) diff --git a/auth/auth.go b/auth/auth.go index 90d76f1..4d30deb 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -25,13 +25,9 @@ const ( StateSudo ) -func IsLoggedIn(s State) bool { - return s >= StateExtended -} +func (s State) IsLoggedIn() bool { return s >= StateExtended } -func IsSudoAvailable(s State) bool { - return s == StateSudo -} +func (s State) IsSudoAvailable() bool { return s == StateSudo } type Provider interface { // AccessState defines the state at which the provider is allowed to show. diff --git a/auth/providers/login.go b/auth/providers/login.go index 11b1c10..e21c222 100644 --- a/auth/providers/login.go +++ b/auth/providers/login.go @@ -4,8 +4,10 @@ import ( "context" "database/sql" "errors" + "fmt" "github.com/1f349/lavender/auth" "github.com/1f349/lavender/database" + "html/template" "net/http" ) @@ -20,13 +22,13 @@ type BasicLogin struct { DB basicLoginDB } -func (b *BasicLogin) Factor() auth.State { return FactorBasic } +func (b *BasicLogin) AccessState() auth.State { return auth.StateUnauthorized } func (b *BasicLogin) Name() string { return "basic" } -func (b *BasicLogin) RenderData(ctx context.Context, req *http.Request, user *database.User, data map[string]any) error { - data["username"] = req.FormValue("username") - return nil +func (b *BasicLogin) RenderTemplate(ctx context.Context, req *http.Request, user *database.User) (template.HTML, error) { + // TODO(melon): rewrite this + return template.HTML(fmt.Sprintf("
%s
", req.FormValue("username"))), nil } func (b *BasicLogin) AttemptLogin(ctx context.Context, req *http.Request, user *database.User) error { @@ -39,7 +41,7 @@ func (b *BasicLogin) AttemptLogin(ctx context.Context, req *http.Request, user * login, err := b.DB.CheckLogin(ctx, un, pw) switch { case err == nil: - return auth.lookupUser(ctx, b.DB, login.Subject, false, user) + return auth.LookupUser(ctx, b.DB, login.Subject, user) case errors.Is(err, sql.ErrNoRows): return auth.BasicUserSafeError(http.StatusForbidden, "Username or password is invalid") default: diff --git a/auth/providers/oauth.go b/auth/providers/oauth.go index 862d069..855ffd5 100644 --- a/auth/providers/oauth.go +++ b/auth/providers/oauth.go @@ -9,6 +9,7 @@ import ( "github.com/1f349/lavender/issuer" "github.com/google/uuid" "golang.org/x/oauth2" + "html/template" "net/http" "time" ) @@ -33,13 +34,12 @@ func (o OAuthLogin) Init() { o.flow = cache.New[string, flowStateData]() } -func (o OAuthLogin) Factor() auth.State { return FactorBasic } +func (o OAuthLogin) AccessState() auth.State { return auth.StateUnauthorized } func (o OAuthLogin) Name() string { return "oauth" } -func (o OAuthLogin) RenderData(ctx context.Context, req *http.Request, user *database.User, data map[string]any) error { - //TODO implement me - panic("implement me") +func (o OAuthLogin) RenderTemplate(ctx context.Context, req *http.Request, user *database.User) (template.HTML, error) { + return "
OAuth Login Template
", nil } func (o OAuthLogin) AttemptLogin(ctx context.Context, req *http.Request, user *database.User) error { diff --git a/auth/providers/otp.go b/auth/providers/otp.go index 14d6f47..ed1279b 100644 --- a/auth/providers/otp.go +++ b/auth/providers/otp.go @@ -3,9 +3,11 @@ package providers import ( "context" "errors" + "fmt" "github.com/1f349/lavender/auth" "github.com/1f349/lavender/database" "github.com/xlzd/gotp" + "html/template" "net/http" "time" ) @@ -24,28 +26,28 @@ type OtpLogin struct { DB otpLoginDB } -func (o *OtpLogin) Factor() auth.State { return FactorExtended } +func (o *OtpLogin) AccessState() auth.State { return auth.StateBasic } func (o *OtpLogin) Name() string { return "basic" } -func (o *OtpLogin) RenderData(_ context.Context, _ *http.Request, user *database.User, data map[string]any) error { +func (o *OtpLogin) RenderTemplate(_ context.Context, _ *http.Request, user *database.User) (template.HTML, error) { if user == nil || user.Subject == "" { - return ErrRequiresPreviousFactor + return "", fmt.Errorf("requires previous factor") } if user.OtpSecret == "" || !isDigitsSupported(user.OtpDigits) { - return auth.ErrUserDoesNotSupportFactor + return "", fmt.Errorf("user does not support factor") } // no need to provide render data - return nil + return "
OTP login template
", nil } func (o *OtpLogin) AttemptLogin(ctx context.Context, req *http.Request, user *database.User) error { if user == nil || user.Subject == "" { - return ErrRequiresPreviousFactor + return fmt.Errorf("requires previous factor") } if user.OtpSecret == "" || !isDigitsSupported(user.OtpDigits) { - return auth.ErrUserDoesNotSupportFactor + return fmt.Errorf("user does not support factor") } code := req.FormValue("code") diff --git a/auth/providers/passkey.go b/auth/providers/passkey.go index 623bae9..642f5a4 100644 --- a/auth/providers/passkey.go +++ b/auth/providers/passkey.go @@ -2,13 +2,15 @@ package providers import ( "context" + "fmt" "github.com/1f349/lavender/auth" "github.com/1f349/lavender/database" + "html/template" "net/http" ) type passkeyLoginDB interface { - auth.lookupUserDB + auth.LookupUserDB } var _ auth.Provider = (*PasskeyLogin)(nil) @@ -17,19 +19,18 @@ type PasskeyLogin struct { DB passkeyLoginDB } -func (p *PasskeyLogin) Factor() auth.State { return FactorBasic } +func (p *PasskeyLogin) AccessState() auth.State { return auth.StateUnauthorized } func (p *PasskeyLogin) Name() string { return "passkey" } -func (p *PasskeyLogin) RenderData(ctx context.Context, req *http.Request, user *database.User, data map[string]any) error { +func (p *PasskeyLogin) RenderTemplate(ctx context.Context, req *http.Request, user *database.User) (template.HTML, error) { if user == nil || user.Subject == "" { - return ErrRequiresPreviousFactor + return "", fmt.Errorf("requires previous factor") } if user.OtpSecret == "" { - return auth.ErrUserDoesNotSupportFactor + return "", fmt.Errorf("user does not support factor") } - //TODO implement me panic("implement me") } @@ -41,7 +42,7 @@ func init() { func (p *PasskeyLogin) AttemptLogin(ctx context.Context, req *http.Request, user *database.User) error { if user.Subject == "" && !passkeyShortcut { - return ErrRequiresPreviousFactor + return fmt.Errorf("requires previous factor") } //TODO implement me diff --git a/auth/userauth.go b/auth/userauth.go index eff31c6..9d42b53 100644 --- a/auth/userauth.go +++ b/auth/userauth.go @@ -22,7 +22,7 @@ func (u UserAuth) NextFlowUrl(origin *url.URL) *url.URL { if origin.Path == "/login" || origin.Path == "/callback" { return nil } - if u.Factor < FactorAuthorized { + if !u.Factor.IsLoggedIn() { return PrepareRedirectUrl("/login", origin) } return nil diff --git a/server/auth_test.go b/server/auth_test.go index 384e90c..5a8f6a0 100644 --- a/server/auth_test.go +++ b/server/auth_test.go @@ -18,7 +18,7 @@ func TestUserAuth_NextFlowUrl(t *testing.T) { assert.Equal(t, url.URL{Path: "/login"}, *u.NextFlowUrl(&url.URL{})) assert.Equal(t, url.URL{Path: "/login", RawQuery: url.Values{"redirect": {"/hello"}}.Encode()}, *u.NextFlowUrl(&url.URL{Path: "/hello"})) assert.Equal(t, url.URL{Path: "/login", RawQuery: url.Values{"redirect": {"/hello?a=A"}}.Encode()}, *u.NextFlowUrl(&url.URL{Path: "/hello", RawQuery: url.Values{"a": {"A"}}.Encode()})) - u.Factor = auth.FactorAuthorized + u.Factor = auth.StateExtended assert.Nil(t, u.NextFlowUrl(&url.URL{})) } diff --git a/server/login.go b/server/login.go index a1b8b17..992700c 100644 --- a/server/login.go +++ b/server/login.go @@ -47,10 +47,11 @@ func (h *httpServer) testAuthSources(req *http.Request, user *database.User, fac data := make(map[string]any) for _, i := range h.authSources { // ignore not-supported factors - if i.State()&factor == 0 { + if i.AccessState() != factor { continue } - err := i.RenderTemplate(req.Context(), req, user, data) + page, err := i.RenderTemplate(req.Context(), req, user) + _ = page authSource[i.Name()] = err == nil clear(data) } @@ -77,14 +78,14 @@ func (h *httpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httpr return } - fmt.Printf("%#v\n", h.testAuthSources(req, userPtr, auth2.FactorBasic)) + fmt.Printf("%#v\n", h.testAuthSources(req, userPtr, auth2.StateBasic)) web.RenderPageTemplate(rw, "login-memory", map[string]any{ "ServiceName": h.conf.ServiceName, "LoginName": cookie.Value, "Redirect": req.URL.Query().Get("redirect"), "Source": "start", - "Auth": h.testAuthSources(req, userPtr, auth2.FactorBasic), + "Auth": h.testAuthSources(req, userPtr, auth2.StateBasic), }) return } @@ -95,7 +96,7 @@ func (h *httpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httpr "LoginName": "", "Redirect": req.URL.Query().Get("redirect"), "Source": "start", - "Auth": h.testAuthSources(req, nil, auth2.FactorBasic), + "Auth": h.testAuthSources(req, nil, auth2.StateBasic), }) } @@ -207,7 +208,7 @@ func (h *httpServer) updateExternalUserInfo(req *http.Request, sso *issuer.WellK }) return auth2.UserAuth{ Subject: userSubject, - Factor: auth2.FactorAuthorized, + Factor: auth2.StateExtended, UserInfo: sessionData.UserInfo, }, err case errors.Is(err, sql.ErrNoRows): @@ -263,7 +264,7 @@ func (h *httpServer) updateExternalUserInfo(req *http.Request, sso *issuer.WellK // TODO(melon): this feels bad sessionData = auth2.UserAuth{ Subject: userSubject, - Factor: auth2.FactorAuthorized, + Factor: auth2.StateExtended, UserInfo: sessionData.UserInfo, } @@ -453,7 +454,7 @@ func (h *httpServer) fetchUserInfo(sso *issuer.WellKnownOIDC, token *oauth2.Toke return auth2.UserAuth{ Subject: subject, - Factor: auth2.FactorAuthorized, + Factor: auth2.StateExtended, UserInfo: userInfoJson, }, nil } diff --git a/web/src/layouts/Layout.astro b/web/src/layouts/Layout.astro index 39204d2..b7b035b 100644 --- a/web/src/layouts/Layout.astro +++ b/web/src/layouts/Layout.astro @@ -1,5 +1,7 @@ --- import Header from "../components/Header.astro"; + +const {title} = Astro.props; --- @@ -9,7 +11,7 @@ import Header from "../components/Header.astro"; - [[.ServiceName]] + [[ renderTitle "{title}" .ServiceName ]]
@@ -36,6 +38,16 @@ import Header from "../components/Header.astro"; background: #13151a; } + main { + margin: auto; + padding: 1rem; + width: 800px; + max-width: calc(100% - 2rem); + color: white; + font-size: 20px; + line-height: 1.6; + } + code { font-family: Menlo, Monaco, diff --git a/web/src/pages/edit.go.html b/web/src/pages/edit.astro similarity index 97% rename from web/src/pages/edit.go.html rename to web/src/pages/edit.astro index ef67d3d..5021351 100644 --- a/web/src/pages/edit.go.html +++ b/web/src/pages/edit.astro @@ -1,3 +1,12 @@ +--- +import Layout from "../layouts/Layout.astro"; +--- + + + + + + diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro index fb62628..0760d27 100644 --- a/web/src/pages/index.astro +++ b/web/src/pages/index.astro @@ -1,123 +1,21 @@ --- import Layout from '../layouts/Layout.astro'; -import Card from '../components/Card.astro'; --- -
- -

Welcome to Astro

-

- To get started, open the directory src/pages in your project.
- Code Challenge: Tweak the "Welcome to Astro" message above. -

- -
+
+
Logged in as: [[ .Auth.UserInfo.name ]] ([[ .Auth.Subject ]])
+
+ +
+ [[ if .IsAdmin ]] +
+ +
+ [[ end ]] +
+ + +
+
- - diff --git a/web/web.go b/web/web.go index cc79f32..d567d9d 100644 --- a/web/web.go +++ b/web/web.go @@ -11,6 +11,7 @@ import ( "io/fs" "net/http" "os" + "path" "path/filepath" "strings" ) @@ -45,14 +46,20 @@ func LoadPages(wd string) error { pageTemplates, err = template.New("web").Delims("[[", "]]").Funcs(template.FuncMap{ "emailHide": utils.EmailHide, - }).ParseFS(webCombinedDir, "*.html") + "renderTitle": + }).ParseFS(webCombinedDir, "*/index.html") return err }) } +func renderTitle(title string, service string) string { + +} + func RenderPageTemplate(wr io.Writer, name string, data any) { - err := pageTemplates.ExecuteTemplate(wr, name+".html", data) + p := path.Join(name, "index.html") + err := pageTemplates.ExecuteTemplate(wr, p, data) if err != nil { logger.Logger.Warn("Failed to render page", "name", name, "err", err) }