I guess I modified stuff

This commit is contained in:
Melon 2025-01-25 19:49:57 +00:00
parent 3fbe905cb2
commit c27a86010c
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
25 changed files with 201 additions and 174 deletions

View File

@ -1,15 +1,12 @@
package auth
import (
"context"
"html/template"
"net/http"
"github.com/1f349/lavender/auth/authContext"
)
type Button interface {
// ButtonName defines the text to show on the button.
ButtonName() string
Provider
// RenderButtonTemplate returns a template for the button widget.
RenderButtonTemplate(ctx context.Context, req *http.Request) template.HTML
RenderButtonTemplate(ctx authContext.TemplateContext)
}

13
auth/auth-form.go Normal file
View File

@ -0,0 +1,13 @@
package auth
import "github.com/1f349/lavender/auth/authContext"
type Form interface {
Provider
// RenderTemplate returns HTML to embed in the page template.
RenderTemplate(ctx authContext.TemplateContext) error
// AttemptLogin processes the login request.
AttemptLogin(ctx authContext.FormContext) error
}

View File

@ -2,11 +2,7 @@ package auth
import (
"context"
"errors"
"fmt"
"github.com/1f349/lavender/auth/authContext"
"github.com/1f349/lavender/database"
"net/http"
)
// State defines the currently reached authentication state
@ -15,6 +11,9 @@ type State byte
const (
// StateUnauthorized defines the "unauthorized" state of a session
StateUnauthorized State = iota
// StateBase defines the "username" only user state
// This state is for providing a username to allow redirecting to oauth clients
StateBase
// StateBasic defines the "username and password with no OTP" user state
// This is skipped if OTP/passkey is optional and not enabled for the user
StateBasic
@ -36,53 +35,6 @@ type Provider interface {
// Name defines a string value for the provider.
Name() string
// RenderTemplate returns HTML to embed in the page template.
RenderTemplate(ctx authContext.TemplateContext) error
// AttemptLogin processes the login request.
AttemptLogin(ctx authContext.TemplateContext) error
}
type UserSafeError struct {
Display string
Code int
Internal error
}
func (e UserSafeError) Error() string {
return fmt.Sprintf("%s [%d]: %v", e.Display, e.Code, e.Internal)
}
func (e UserSafeError) Unwrap() error {
return e.Internal
}
func BasicUserSafeError(code int, message string) UserSafeError {
return UserSafeError{
Code: code,
Display: message,
Internal: errors.New(message),
}
}
func AdminSafeError(inner error) UserSafeError {
return UserSafeError{
Code: http.StatusInternalServerError,
Display: "Internal server error",
Internal: inner,
}
}
type RedirectError struct {
Target string
Code int
}
func (e RedirectError) TargetUrl() string { return e.Target }
func (e RedirectError) Error() string {
return fmt.Sprintf("redirect to '%s'", e.Target)
}
type LookupUserDB interface {

View File

@ -2,11 +2,12 @@ package authContext
import (
"context"
"github.com/1f349/lavender/auth/login-process"
"github.com/1f349/lavender/database"
"net/http"
)
func NewTemplateContext(req *http.Request, user *database.User) TemplateContext {
func NewTemplateContext(req *http.Request, user *database.User) *BaseTemplateContext {
return &BaseTemplateContext{
req: req,
user: user,
@ -18,30 +19,38 @@ type TemplateContext interface {
Request() *http.Request
User() *database.User
Render(data any)
Data() any
}
type FormContext interface {
Context() context.Context
Request() *http.Request
User() *database.User
SetUser(user *database.User)
Render(data any)
UpdateSession(data login_process.LoginProcessData)
}
type ButtonContext interface {
Context() context.Context
Request() *http.Request
Render(data any)
}
var _ TemplateContext = &BaseTemplateContext{}
type BaseTemplateContext struct {
req *http.Request
user *database.User
data any
}
func (t *BaseTemplateContext) Context() context.Context {
return t.req.Context()
}
func (t *BaseTemplateContext) Context() context.Context { return t.req.Context() }
func (t *BaseTemplateContext) Request() *http.Request {
return t.req
}
func (t *BaseTemplateContext) Request() *http.Request { return t.req }
func (t *BaseTemplateContext) User() *database.User {
return t.user
}
func (t *BaseTemplateContext) User() *database.User { return t.user }
func (t *BaseTemplateContext) Render(data any) {
t.data = data
}
func (t *BaseTemplateContext) Render(data any) { t.data = data }
func (t *BaseTemplateContext) Data() any {
return t.data

View File

@ -0,0 +1,14 @@
package login_process
import "github.com/1f349/mjwt"
var _ mjwt.Claims = (*LoginProcessData)(nil)
// TODO: add some actual session management
type LoginProcessData struct {
State byte
}
func (d LoginProcessData) Valid() error { return nil }
func (d LoginProcessData) Type() string { return "login-process" }

47
auth/providers/base.go Normal file
View File

@ -0,0 +1,47 @@
package providers
import (
"github.com/1f349/lavender/auth"
"github.com/1f349/lavender/auth/authContext"
"github.com/1f349/lavender/logger"
)
var _ auth.Provider = (*InitialLogin)(nil)
type InitialLogin struct{}
func (m *InitialLogin) AccessState() auth.State { return auth.StateUnauthorized }
func (m *InitialLogin) Name() string { return "base" }
func (m *InitialLogin) RenderTemplate(ctx authContext.FormContext) error {
type s struct {
UserEmail string
Redirect string
}
req := ctx.Request()
q := req.URL.Query()
cookie, err := req.Cookie("lavender-user-memory")
if err == nil && cookie.Valid() == nil {
ctx.Render(s{
UserEmail: cookie.Value,
Redirect: q.Get("redirect"),
})
return nil
}
ctx.Render(s{
UserEmail: "",
Redirect: q.Get("redirect"),
})
return nil
}
func (m *InitialLogin) AttemptLogin(ctx authContext.FormContext) error {
req := ctx.Request()
userEmail := req.FormValue("email")
rememberMe := req.FormValue("remember-me")
logger.Logger.Debug("Hi", "em", userEmail, "rm", rememberMe)
return nil
}

View File

@ -1,33 +0,0 @@
package providers
import (
"github.com/1f349/lavender/auth"
"github.com/1f349/lavender/auth/authContext"
"github.com/1f349/lavender/web"
)
var _ auth.Provider = (*MemoryLogin)(nil)
type MemoryLogin struct{}
func (m *MemoryLogin) AccessState() auth.State { return auth.StateUnauthorized }
func (m *MemoryLogin) Name() string { return "memory" }
func (m *MemoryLogin) RenderTemplate(ctx authContext.TemplateContext) error {
cookie, err := ctx.Request().Cookie("lavender-user-memory")
if err == nil && cookie.Valid() == nil {
ctx.Render(struct {
ServiceName string
LoginName string
Redirect string
}{
ServiceName: ,
})
}
}
func (m *MemoryLogin) AttemptLogin(ctx authContext.TemplateContext) error {
//TODO implement me
panic("implement me")
}

View File

@ -47,13 +47,7 @@ func (o OAuthLogin) AccessState() auth.State { return auth.StateUnauthorized }
func (o OAuthLogin) Name() string { return "oauth" }
func (o OAuthLogin) RenderTemplate(ctx authContext.TemplateContext) error {
// TODO: does this need to exist?
ctx.Render(map[string]any{"Error": "no"})
return nil
}
func (o OAuthLogin) AttemptLogin(ctx authContext.TemplateContext) error {
func (o OAuthLogin) AttemptLogin(ctx authContext.FormContext) error {
rCtx := ctx.Context()
login, ok := rCtx.Value(oauthServiceLogin(0)).(*issuer.WellKnownOIDC)

View File

@ -30,7 +30,7 @@ func (o *OtpLogin) AccessState() auth.State { return auth.StateBasic }
func (o *OtpLogin) Name() string { return "basic" }
func (o *OtpLogin) RenderTemplate(ctx authContext.TemplateContext) error {
func (o *OtpLogin) RenderTemplate(ctx authContext.FormContext) error {
user := ctx.User()
if user == nil || user.Subject == "" {
return fmt.Errorf("requires previous factor")
@ -48,7 +48,7 @@ func (o *OtpLogin) RenderTemplate(ctx authContext.TemplateContext) error {
return nil
}
func (o *OtpLogin) AttemptLogin(ctx authContext.TemplateContext) error {
func (o *OtpLogin) AttemptLogin(ctx authContext.FormContext) error {
user := ctx.User()
if user == nil || user.Subject == "" {
return fmt.Errorf("requires previous factor")

View File

@ -2,9 +2,7 @@ package providers
import (
"context"
"fmt"
"github.com/1f349/lavender/auth"
"github.com/1f349/lavender/auth/authContext"
"html/template"
"net/http"
)
@ -26,38 +24,6 @@ func (p *PasskeyLogin) AccessState() auth.State { return auth.StateUnauthorized
func (p *PasskeyLogin) Name() string { return "passkey" }
func (p *PasskeyLogin) RenderTemplate(ctx authContext.TemplateContext) error {
user := ctx.User()
if user == nil || user.Subject == "" {
return fmt.Errorf("requires previous factor")
}
if user.OtpSecret == "" {
return fmt.Errorf("user does not support factor")
}
panic("implement me")
}
var passkeyShortcut = true
func init() {
passkeyShortcut = true
}
func (p *PasskeyLogin) AttemptLogin(ctx authContext.TemplateContext) error {
user := ctx.User()
if user.Subject == "" && !passkeyShortcut {
return fmt.Errorf("requires previous factor")
}
//TODO implement me
panic("implement me")
}
func (p *PasskeyLogin) ButtonName() string {
return "Login with Passkey"
}
func (p *PasskeyLogin) RenderButtonTemplate(ctx context.Context, req *http.Request) template.HTML {
return "<div>Passkey Button</div>"
}

View File

@ -10,22 +10,25 @@ import (
"net/http"
)
type basicLoginDB interface {
type passwordLoginDB interface {
auth.LookupUserDB
CheckLogin(ctx context.Context, un, pw string) (database.CheckLoginResult, error)
}
var _ auth.Provider = (*BasicLogin)(nil)
var (
_ auth.Provider = (*PasswordLogin)(nil)
_ auth.Form = (*PasswordLogin)(nil)
)
type BasicLogin struct {
DB basicLoginDB
type PasswordLogin struct {
DB passwordLoginDB
}
func (b *BasicLogin) AccessState() auth.State { return auth.StateUnauthorized }
func (b *PasswordLogin) AccessState() auth.State { return auth.StateBase }
func (b *BasicLogin) Name() string { return "basic" }
func (b *PasswordLogin) Name() string { return "password" }
func (b *BasicLogin) RenderTemplate(ctx authContext.TemplateContext) error {
func (b *PasswordLogin) RenderTemplate(ctx authContext.TemplateContext) error {
// TODO(melon): rewrite this
req := ctx.Request()
un := req.FormValue("login")
@ -43,7 +46,7 @@ func (b *BasicLogin) RenderTemplate(ctx authContext.TemplateContext) error {
return nil
}
func (b *BasicLogin) AttemptLogin(ctx authContext.TemplateContext) error {
func (b *PasswordLogin) AttemptLogin(ctx authContext.FormContext) error {
req := ctx.Request()
un := req.FormValue("username")
pw := req.FormValue("password")

14
auth/redirect-error.go Normal file
View File

@ -0,0 +1,14 @@
package auth
import "fmt"
type RedirectError struct {
Target string
Code int
}
func (e RedirectError) TargetUrl() string { return e.Target }
func (e RedirectError) Error() string {
return fmt.Sprintf("redirect to '%s'", e.Target)
}

37
auth/user-safe-error.go Normal file
View File

@ -0,0 +1,37 @@
package auth
import (
"errors"
"fmt"
"net/http"
)
type UserSafeError struct {
Display string
Code int
Internal error
}
func (e UserSafeError) Error() string {
return fmt.Sprintf("%s [%d]: %v", e.Display, e.Code, e.Internal)
}
func (e UserSafeError) Unwrap() error {
return e.Internal
}
func BasicUserSafeError(code int, message string) UserSafeError {
return UserSafeError{
Code: code,
Display: message,
Internal: errors.New(message),
}
}
func AdminSafeError(inner error) UserSafeError {
return UserSafeError{
Code: http.StatusInternalServerError,
Display: "Internal server error",
Internal: inner,
}
}

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.28.0
package database

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.28.0
// source: manage-oauth.sql
package database

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.28.0
// source: manage-users.sql
package database

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.28.0
package database

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.28.0
// source: otp.sql
package database

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.28.0
// source: profiles.sql
package database

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.28.0
// source: roles.sql
package database

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.28.0
// source: users.sql
package database

View File

@ -54,7 +54,11 @@ func (h *httpServer) testAuthSources(req *http.Request, user *database.User, fac
if i.AccessState() != factor {
continue
}
err := i.RenderTemplate(authContext.NewTemplateContext(req, user))
form, ok := i.(auth.Form)
if !ok {
continue
}
err := form.RenderTemplate(authContext.NewTemplateContext(req, user))
authSource[i.Name()] = err == nil
clear(data)
}
@ -70,7 +74,7 @@ func (h *httpServer) getAuthWithState(state auth.State) auth.Provider {
return nil
}
func (h *httpServer) renderAuthTemplate(req *http.Request, provider auth.Provider) (template.HTML, error) {
func (h *httpServer) renderAuthTemplate(req *http.Request, provider auth.Form) (template.HTML, error) {
tmpCtx := authContext.NewTemplateContext(req, new(database.User))
err := provider.RenderTemplate(tmpCtx)
@ -119,9 +123,18 @@ func (h *httpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httpr
return
}
buttonTemplates := make([]template.HTML, len(h.authButtons))
buttonCtx := authContext.NewTemplateContext(req, new(database.User))
buttonTemplates := make([]template.HTML, 0, len(h.authButtons))
for i := range h.authButtons {
buttonTemplates[i] = h.authButtons[i].RenderButtonTemplate(req.Context(), req)
h.authButtons[i].RenderButtonTemplate(buttonCtx)
if buttonCtx.Data() != nil {
// TODO: finish the buttons here
buf := new(bytes.Buffer)
web.RenderPageTemplate(buf, "auth-buttons/"+h.authButtons[i].Name(), buttonCtx.Data())
buttonTemplates = append(buttonTemplates, template.HTML(buf.String()))
buttonCtx.Render(nil)
}
}
type loginError struct {
@ -133,8 +146,9 @@ func (h *httpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httpr
provider := h.getAuthWithState(auth.StateUnauthorized)
// Maybe the admin has disabled some login providers but does have a button based provider available?
if provider != nil {
renderTemplate, err = h.renderAuthTemplate(req, provider)
form, ok := provider.(auth.Form)
if provider != nil && ok {
renderTemplate, err = h.renderAuthTemplate(req, form)
if err != nil {
logger.Logger.Warn("No provider for login")
web.RenderPageTemplate(rw, "login-error", loginError{Error: "No available provider for login"})

View File

@ -55,7 +55,7 @@ type mailLinkKey struct {
func SetupRouter(r *httprouter.Router, config conf.Conf, mailSender *mail.Mail, db *database.Queries, signingKey *mjwt.Issuer) {
// TODO: move auth provider init to main function
// TODO: allow dynamically changing the providers based on database information
authBasic := &providers.BasicLogin{DB: db}
authBasic := &providers.PasswordLogin{DB: db}
authOtp := &providers.OtpLogin{DB: db}
authOAuth := &providers.OAuthLogin{DB: db, BaseUrl: &config.BaseUrl}
authOAuth.Init()

View File

@ -8,5 +8,8 @@ export const partial = true;
<label for="field_email">Email:</label>
<input type="email" name="email" id="field_email" value="[[.UserEmail]]" required/>
</div>
<div>
<label>Remember Me? <input type="checkbox" name="remember-me"/></label>
</div>
<button type="submit">Login</button>
</form>

View File

@ -4,10 +4,7 @@ export const partial = true;
<form method="POST" action="/login">
<input type="hidden" name="redirect" value="[[.Redirect]]"/>
<div>
<label for="field_email">Email:</label>
<input type="email" name="email" id="field_email" value="[[.UserEmail]]" required/>
</div>
<input type="hidden" name="email" value="[[.UserEmail]]"/>
<div>
<label for="field_password">Password:</label>
<input type="password" name="password" id="field_password" autofocus required/>