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

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>