auth: Improve login parts

This commit is contained in:
Tom Wiesing 2022-12-25 11:54:16 +01:00
parent 1af9d0d83f
commit 515142c055
No known key found for this signature in database
13 changed files with 382 additions and 101 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,12 +1,15 @@
package auth
import (
"context"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
"github.com/julienschmidt/httprouter"
)
type Auth struct {
@ -22,3 +25,30 @@ type Auth struct {
var (
_ component.Routeable = (*Auth)(nil)
)
func (auth *Auth) Routes() []string { return []string{"/auth/"} }
func (auth *Auth) HandleRoute(ctx context.Context, route string) (http.Handler, error) {
router := httprouter.New()
// setup the csrf handler (if needed)
auth.csrf.Get(func() func(http.Handler) http.Handler {
var opts []csrf.Option
if !auth.Config.HTTPSEnabled() {
opts = append(opts, csrf.Secure(false))
}
opts = append(opts, csrf.Path(route))
return csrf.Protect(auth.Config.CSRFSecret(), opts...)
})
router.Handler(http.MethodGet, route, auth.authHome(ctx))
login := auth.loginForm()
router.Handler(http.MethodGet, route+"login", login)
router.Handler(http.MethodPost, route+"login", login)
router.HandlerFunc(http.MethodGet, route+"logout", auth.logoutRoute)
return router, nil
}

View file

@ -0,0 +1,24 @@
package auth
import (
"context"
_ "embed"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
)
//go:embed "templates/home.html"
var homeHTMLStr string
var homeTemplate = static.AssetsAuthHome.MustParseShared(
"home.html",
homeHTMLStr,
)
func (auth *Auth) authHome(ctx context.Context) http.Handler {
return auth.Protect(&httpx.HTMLHandler[*AuthUser]{
Handler: auth.UserOf,
Template: homeTemplate,
}, nil)
}

View file

@ -8,17 +8,11 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
"github.com/julienschmidt/httprouter"
_ "embed"
)
func (auth *Auth) Routes() []string {
return []string{"/auth/"}
}
type contextUserKey struct{}
var ctxUserKey = contextUserKey{}
@ -109,32 +103,9 @@ var loginResponse = httpx.Response{
Body: []byte("user is signed in"),
}
// HandleRoute returns the handler for the requested route
func (auth *Auth) HandleRoute(ctx context.Context, route string) (http.Handler, error) {
router := httprouter.New()
csrf := auth.csrf.Get(func() func(http.Handler) http.Handler {
var opts []csrf.Option
if !auth.Config.HTTPSEnabled() {
opts = append(opts, csrf.Secure(false))
}
opts = append(opts, csrf.Path(route))
return csrf.Protect(auth.Config.CSRFSecret(), opts...)
})
router.Handler(http.MethodGet, route, auth.Protect(loginResponse, nil))
router.HandlerFunc(http.MethodGet, route+"login", auth.loginRoute)
router.HandlerFunc(http.MethodPost, route+"login", auth.loginRoute)
router.HandlerFunc(http.MethodGet, route+"logout", auth.logoutRoute)
return csrf(router), nil
}
type loginContext struct {
Message string
CSRF template.HTML
Form template.HTML
}
// Protect returns a new handler which requires a user to be logged in and pass the perm function.
@ -152,7 +123,6 @@ func (auth *Auth) Protect(handler http.Handler, perm func(user *AuthUser, r *htt
// if there is no user in the session
// we need to login the user
if user == nil {
// we can't redirect anything other than GET
// (because it might be a form)
// => so we just return a forbidden
@ -190,70 +160,68 @@ func (auth *Auth) Protect(handler http.Handler, perm func(user *AuthUser, r *htt
})
}
func (auth *Auth) loginRoute(w http.ResponseWriter, r *http.Request) {
var message string
// loginForm returns the login form handler.
// auth.csrf must have been populated
func (auth *Auth) loginForm() *httpx.Form[*AuthUser] {
return &httpx.Form[*AuthUser]{
Fields: []httpx.Field{
{Name: "username", Type: httpx.TextField},
{Name: "password", Type: httpx.PasswordField},
},
// try to read a user from the session
user, err := auth.UserOf(r)
if err != nil {
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
return
CSRF: auth.csrf.Get(nil),
RenderForm: func(template template.HTML, err error, w http.ResponseWriter, r *http.Request) {
ctx := loginContext{
Message: "",
Form: template,
}
if err != nil {
ctx.Message = "Login Failed"
}
httpx.WriteHTML(ctx, nil, loginTemplate, "", w, r)
},
Validate: func(ctx context.Context, values map[string]string) (*AuthUser, error) {
username, password := values["username"], values["password"]
// make sure that the user exists
user, err := auth.User(ctx, username)
if err != nil {
return nil, err
}
// check the password (TODO: Support TOTP)
err = user.CheckPassword(ctx, []byte(password))
if err != nil {
return nil, err
}
return user, nil
},
SkipForm: func(r *http.Request) (user *AuthUser, skip bool) {
user, err := auth.UserOf(r)
return user, err == nil && user != nil
},
RenderSuccess: func(user *AuthUser, _ map[string]string, w http.ResponseWriter, r *http.Request) error {
if err := auth.writeLogin(w, r, user); err != nil {
return err
}
// get the destination
next := r.URL.Query().Get("next")
if next == "" || next[0] != '/' {
next = "/"
}
// and redirect to it!
http.Redirect(w, r, next, http.StatusSeeOther)
return nil
},
}
if user != nil {
goto success
}
switch r.Method {
default:
panic("never reached")
case http.MethodGet:
goto form
case http.MethodPost:
// parse the form!
if err := r.ParseForm(); err != nil {
message = "Login failed"
goto form
}
// get the username and password
username := r.Form.Get("username")
password := r.Form.Get("password")
// make sure that the user exists
user, err := auth.User(r.Context(), username)
if err != nil {
message = "Login failed"
goto form
}
// check the password (TODO: Support TOTP)
err = user.CheckPassword(r.Context(), []byte(password))
if err != nil {
message = "Login failed"
goto form
}
// and we logged the user in!
auth.writeLogin(w, r, user)
goto success
}
form:
httpx.WriteHTML(loginContext{
Message: message,
CSRF: csrf.TemplateField(r),
}, nil, loginTemplate, "", w, r)
return
success:
// get the destination
next := r.URL.Query().Get("next")
if next == "" || next[0] != '/' {
next = "/"
}
// and redirect to it!
http.Redirect(w, r, next, http.StatusSeeOther)
}
func (auth *Auth) logoutRoute(w http.ResponseWriter, r *http.Request) {

View file

@ -0,0 +1,29 @@
{{ template "_base.html" . }}
{{ define "title" }}Distillery User{{ end }}
{{ define "header/time" }}
<!-- no header/time -->
{{ end }}
{{ define "header"}}
<!-- no header -->
{{ end }}
{{ define "content" }}
<div class="pure-u-1">
<div>
Welcome {{ .User.User }}!
</div>
<form method="GET" action="/auth/password/">
<input type="submit" value="Change Password"></input>
</form>
<!-- TODO: A logout button only for now -->
<form method="GET" action="/auth/logout/">
<input type="submit" value="Logout"></input>
</form>
</div>
{{ end }}

View file

@ -20,10 +20,7 @@
<form class="pure-form" method="POST">
<fieldset>
<legend>Login Required</legend>
<input type="text" name="username">
<input type="password" name="password">
{{ .CSRF }}
{{ .Form }}
<input type="submit" value="Login">
</fieldset>
</form>

View file

@ -21,7 +21,7 @@ type Assets struct {
Styles string // <link> tags inserted by the asset
}
//go:generate node build.mjs HomeHome ComponentsIndex ControlIndex ControlInstance InstanceComponentsIndex AuthLogin
//go:generate node build.mjs HomeHome ComponentsIndex ControlIndex ControlInstance InstanceComponentsIndex AuthLogin AuthHome
// MustParse parses a new template from the given source
// and calls [RegisterAssoc] on it.

View file

@ -37,3 +37,9 @@ var AssetsAuthLogin = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/AuthLogin.38d394c2.js"></script><script src="/static/AuthLogin.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/AuthLogin.38d394c2.css">`,
}
// AssetsAuthHome contains assets for the 'AuthHome' entrypoint.
var AssetsAuthHome = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/AuthHome.38d394c2.js"></script><script src="/static/AuthHome.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/AuthHome.38d394c2.css">`,
}

View file

@ -0,0 +1 @@
/* nothing for now */

View file

@ -0,0 +1 @@
// nothing for now