diff --git a/auth/auth.go b/auth/auth.go
index d4206c4..90d76f1 100644
--- a/auth/auth.go
+++ b/auth/auth.go
@@ -9,41 +9,44 @@ import (
"net/http"
)
-type Factor byte
+// State defines the currently reached authentication state
+type State byte
const (
- // FactorUnauthorized defines the "unauthorized" state of a session
- FactorUnauthorized Factor = iota
- FactorBasic
- FactorExtended
- FactorSudo
+ // StateUnauthorized defines the "unauthorized" state of a session
+ StateUnauthorized State = iota
+ // 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
+ // StateExtended defines the "logged in" user state
+ StateExtended
+ // StateSudo defines the "sudo" user state
+ // This state is temporary and has a configurable duration
+ StateSudo
)
-type Provider interface {
- // Factor defines the factors potentially supported by the provider
- // Some factors might be unavailable due to user preference
- Factor() Factor
-
- // Name defines a string value for the provider, useful for template switching
- Name() string
-
- // RenderTemplate returns HTML to embed in the page template
- RenderTemplate(ctx context.Context, req *http.Request, user *database.User) (template.HTML, error)
-
- // AttemptLogin processes the login request
- AttemptLogin(ctx context.Context, req *http.Request, user *database.User) error
+func IsLoggedIn(s State) bool {
+ return s >= StateExtended
}
-var (
- // ErrRequiresBasicFactor notifies the ServeHTTP function to ask for another factor
- ErrRequiresBasicFactor = errors.New("requires basic factor")
- // ErrRequiresExtendedFactor is a generic error for providers which require a previous factor
- ErrRequiresExtendedFactor = errors.New("requires extended factor")
+func IsSudoAvailable(s State) bool {
+ return s == StateSudo
+}
- ErrRequiresSudoFactor = errors.New("requires sudo factor")
- // ErrUserDoesNotSupportFactor is a generic error for providers with are unable to support the user
- ErrUserDoesNotSupportFactor = errors.New("user does not support factor")
-)
+type Provider interface {
+ // AccessState defines the state at which the provider is allowed to show.
+ // Some factors might be unavailable due to user preference.
+ AccessState() State
+
+ // Name defines a string value for the provider.
+ Name() string
+
+ // RenderTemplate returns HTML to embed in the page template.
+ RenderTemplate(ctx context.Context, req *http.Request, user *database.User) (template.HTML, error)
+
+ // AttemptLogin processes the login request.
+ AttemptLogin(ctx context.Context, req *http.Request, user *database.User) error
+}
type UserSafeError struct {
Display string
@@ -86,18 +89,15 @@ func (e RedirectError) Error() string {
return fmt.Sprintf("redirect to '%s'", e.Target)
}
-type lookupUserDB interface {
+type LookupUserDB interface {
GetUser(ctx context.Context, subject string) (database.User, error)
}
-func lookupUser(ctx context.Context, db lookupUserDB, subject string, resolvesTwoFactor bool, user *database.User) error {
+func LookupUser(ctx context.Context, db LookupUserDB, subject string, user *database.User) error {
getUser, err := db.GetUser(ctx, subject)
if err != nil {
return err
}
*user = getUser
- if user.NeedFactor && !resolvesTwoFactor {
- return ErrRequiresSecondFactor
- }
return nil
}
diff --git a/auth/login.go b/auth/providers/login.go
similarity index 67%
rename from auth/login.go
rename to auth/providers/login.go
index f58ae2e..11b1c10 100644
--- a/auth/login.go
+++ b/auth/providers/login.go
@@ -1,25 +1,26 @@
-package auth
+package providers
import (
"context"
"database/sql"
"errors"
+ "github.com/1f349/lavender/auth"
"github.com/1f349/lavender/database"
"net/http"
)
type basicLoginDB interface {
- lookupUserDB
+ auth.LookupUserDB
CheckLogin(ctx context.Context, un, pw string) (database.CheckLoginResult, error)
}
-var _ Provider = (*BasicLogin)(nil)
+var _ auth.Provider = (*BasicLogin)(nil)
type BasicLogin struct {
DB basicLoginDB
}
-func (b *BasicLogin) Factor() Factor { return FactorBasic }
+func (b *BasicLogin) Factor() auth.State { return FactorBasic }
func (b *BasicLogin) Name() string { return "basic" }
@@ -32,15 +33,15 @@ func (b *BasicLogin) AttemptLogin(ctx context.Context, req *http.Request, user *
un := req.FormValue("username")
pw := req.FormValue("password")
if len(pw) < 8 {
- return BasicUserSafeError(http.StatusBadRequest, "Password too short")
+ return auth.BasicUserSafeError(http.StatusBadRequest, "Password too short")
}
login, err := b.DB.CheckLogin(ctx, un, pw)
switch {
case err == nil:
- return lookupUser(ctx, b.DB, login.Subject, false, user)
+ return auth.lookupUser(ctx, b.DB, login.Subject, false, user)
case errors.Is(err, sql.ErrNoRows):
- return BasicUserSafeError(http.StatusForbidden, "Username or password is invalid")
+ return auth.BasicUserSafeError(http.StatusForbidden, "Username or password is invalid")
default:
return err
}
diff --git a/auth/oauth.go b/auth/providers/oauth.go
similarity index 86%
rename from auth/oauth.go
rename to auth/providers/oauth.go
index 8c4eddc..862d069 100644
--- a/auth/oauth.go
+++ b/auth/providers/oauth.go
@@ -1,9 +1,10 @@
-package auth
+package providers
import (
"context"
"fmt"
"github.com/1f349/cache"
+ "github.com/1f349/lavender/auth"
"github.com/1f349/lavender/database"
"github.com/1f349/lavender/issuer"
"github.com/google/uuid"
@@ -18,7 +19,7 @@ type flowStateData struct {
redirect string
}
-var _ Provider = (*OAuthLogin)(nil)
+var _ auth.Provider = (*OAuthLogin)(nil)
type OAuthLogin struct {
DB *database.Queries
@@ -32,7 +33,7 @@ func (o OAuthLogin) Init() {
o.flow = cache.New[string, flowStateData]()
}
-func (o OAuthLogin) Factor() Factor { return FactorBasic }
+func (o OAuthLogin) Factor() auth.State { return FactorBasic }
func (o OAuthLogin) Name() string { return "oauth" }
@@ -58,10 +59,10 @@ func (o OAuthLogin) AttemptLogin(ctx context.Context, req *http.Request, user *d
oa2conf.RedirectURL = o.BaseUrl + "/callback"
nextUrl := oa2conf.AuthCodeURL(state, oauth2.SetAuthURLParam("login_name", loginUn))
- return RedirectError{Target: nextUrl, Code: http.StatusFound}
+ return auth.RedirectError{Target: nextUrl, Code: http.StatusFound}
}
-func (o OAuthLogin) OAuthCallback(rw http.ResponseWriter, req *http.Request, info func(req *http.Request, sso *issuer.WellKnownOIDC, token *oauth2.Token) (UserAuth, error), cookie func(rw http.ResponseWriter, authData UserAuth, loginName string) bool, redirect func(rw http.ResponseWriter, req *http.Request)) {
+func (o OAuthLogin) OAuthCallback(rw http.ResponseWriter, req *http.Request, info func(req *http.Request, sso *issuer.WellKnownOIDC, token *oauth2.Token) (auth.UserAuth, error), cookie func(rw http.ResponseWriter, authData auth.UserAuth, loginName string) bool, redirect func(rw http.ResponseWriter, req *http.Request)) {
flowState, ok := o.flow.Get(req.FormValue("state"))
if !ok {
http.Error(rw, "Invalid flow state", http.StatusBadRequest)
diff --git a/auth/otp.go b/auth/providers/otp.go
similarity index 84%
rename from auth/otp.go
rename to auth/providers/otp.go
index 5a1e5d5..14d6f47 100644
--- a/auth/otp.go
+++ b/auth/providers/otp.go
@@ -1,8 +1,9 @@
-package auth
+package providers
import (
"context"
"errors"
+ "github.com/1f349/lavender/auth"
"github.com/1f349/lavender/database"
"github.com/xlzd/gotp"
"net/http"
@@ -17,13 +18,13 @@ type otpLoginDB interface {
GetOtp(ctx context.Context, subject string) (database.GetOtpRow, error)
}
-var _ Provider = (*OtpLogin)(nil)
+var _ auth.Provider = (*OtpLogin)(nil)
type OtpLogin struct {
DB otpLoginDB
}
-func (o *OtpLogin) Factor() Factor { return FactorExtended }
+func (o *OtpLogin) Factor() auth.State { return FactorExtended }
func (o *OtpLogin) Name() string { return "basic" }
@@ -32,7 +33,7 @@ func (o *OtpLogin) RenderData(_ context.Context, _ *http.Request, user *database
return ErrRequiresPreviousFactor
}
if user.OtpSecret == "" || !isDigitsSupported(user.OtpDigits) {
- return ErrUserDoesNotSupportFactor
+ return auth.ErrUserDoesNotSupportFactor
}
// no need to provide render data
@@ -44,13 +45,13 @@ func (o *OtpLogin) AttemptLogin(ctx context.Context, req *http.Request, user *da
return ErrRequiresPreviousFactor
}
if user.OtpSecret == "" || !isDigitsSupported(user.OtpDigits) {
- return ErrUserDoesNotSupportFactor
+ return auth.ErrUserDoesNotSupportFactor
}
code := req.FormValue("code")
if !validateTotp(user.OtpSecret, int(user.OtpDigits), code) {
- return BasicUserSafeError(http.StatusBadRequest, "invalid OTP code")
+ return auth.BasicUserSafeError(http.StatusBadRequest, "invalid OTP code")
}
return nil
}
diff --git a/auth/passkey.go b/auth/providers/passkey.go
similarity index 78%
rename from auth/passkey.go
rename to auth/providers/passkey.go
index ec9ba11..623bae9 100644
--- a/auth/passkey.go
+++ b/auth/providers/passkey.go
@@ -1,22 +1,23 @@
-package auth
+package providers
import (
"context"
+ "github.com/1f349/lavender/auth"
"github.com/1f349/lavender/database"
"net/http"
)
type passkeyLoginDB interface {
- lookupUserDB
+ auth.lookupUserDB
}
-var _ Provider = (*PasskeyLogin)(nil)
+var _ auth.Provider = (*PasskeyLogin)(nil)
type PasskeyLogin struct {
DB passkeyLoginDB
}
-func (p *PasskeyLogin) Factor() Factor { return FactorBasic }
+func (p *PasskeyLogin) Factor() auth.State { return FactorBasic }
func (p *PasskeyLogin) Name() string { return "passkey" }
@@ -25,7 +26,7 @@ func (p *PasskeyLogin) RenderData(ctx context.Context, req *http.Request, user *
return ErrRequiresPreviousFactor
}
if user.OtpSecret == "" {
- return ErrUserDoesNotSupportFactor
+ return auth.ErrUserDoesNotSupportFactor
}
//TODO implement me
diff --git a/auth/userauth.go b/auth/userauth.go
index 9fdd8d7..eff31c6 100644
--- a/auth/userauth.go
+++ b/auth/userauth.go
@@ -11,7 +11,7 @@ type UserHandler func(rw http.ResponseWriter, req *http.Request, params httprout
type UserAuth struct {
Subject string
- Factor Factor
+ Factor State
UserInfo UserInfoFields
}
diff --git a/frontend/.gitignore b/frontend/.gitignore
deleted file mode 100644
index a547bf3..0000000
--- a/frontend/.gitignore
+++ /dev/null
@@ -1,24 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json
deleted file mode 100644
index bdef820..0000000
--- a/frontend/.vscode/extensions.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "recommendations": ["svelte.svelte-vscode"]
-}
diff --git a/frontend/README.md b/frontend/README.md
deleted file mode 100644
index e6cd94f..0000000
--- a/frontend/README.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# Svelte + TS + Vite
-
-This template should help get you started developing with Svelte and TypeScript in Vite.
-
-## Recommended IDE Setup
-
-[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
-
-## Need an official Svelte framework?
-
-Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
-
-## Technical considerations
-
-**Why use this over SvelteKit?**
-
-- It brings its own routing solution which might not be preferable for some users.
-- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
-
-This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
-
-Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
-
-**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
-
-Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
-
-**Why include `.vscode/extensions.json`?**
-
-Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
-
-**Why enable `allowJs` in the TS template?**
-
-While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
-
-**Why is HMR not preserving my local component state?**
-
-HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
-
-If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
-
-```ts
-// store.ts
-// An extremely simple external store
-import { writable } from 'svelte/store'
-export default writable(0)
-```
diff --git a/frontend/index.html b/frontend/index.html
deleted file mode 100644
index b6c5f0a..0000000
--- a/frontend/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-