Add initial auth token / login stuff
This commit is contained in:
parent
acca4fe232
commit
56cd737203
2
go.mod
2
go.mod
|
@ -18,7 +18,7 @@ require (
|
|||
github.com/stretchr/testify v1.7.0
|
||||
github.com/teacat/noire v1.1.0
|
||||
github.com/wellington/go-libsass v0.9.2
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
)
|
||||
|
||||
replace github.com/rs/zerolog v1.20.0 => github.com/bvisness/zerolog v1.20.1-0.20210321191248-05f63bf0e9e0
|
||||
|
|
5
go.sum
5
go.sum
|
@ -305,8 +305,8 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U
|
|||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -375,6 +375,7 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
128
src/auth/auth.go
128
src/auth/auth.go
|
@ -2,61 +2,132 @@ package auth
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
type HashAlgorithm string
|
||||
|
||||
const (
|
||||
PBKDF2_SHA256 = "pbkdf2_sha256"
|
||||
Django_PBKDF2SHA256 HashAlgorithm = "pbkdf2_sha256"
|
||||
Argon2id HashAlgorithm = "argon2id"
|
||||
)
|
||||
|
||||
const PKBDF2KeyLength = 64
|
||||
const saltLength = 16
|
||||
const keyLength = 64
|
||||
|
||||
type HashedPassword struct {
|
||||
Algorithm HashAlgorithm
|
||||
Iterations int
|
||||
Salt string
|
||||
Hash string
|
||||
AlgoConfig string // arbitrary info describing the hash parameters (e.g. work factor)
|
||||
|
||||
// To make it easier to handle varying implementations and encodings,
|
||||
// these fields will always store a form of the data that can be directly
|
||||
// stored in the database (usually base64-encoded or whatever).
|
||||
Salt string
|
||||
Hash string
|
||||
}
|
||||
|
||||
func ParseDjangoPasswordString(s string) (HashedPassword, error) {
|
||||
func ParsePasswordString(s string) (HashedPassword, error) {
|
||||
pieces := strings.SplitN(s, "$", 4)
|
||||
if len(pieces) < 4 {
|
||||
return HashedPassword{}, oops.New(nil, "unrecognized password string format")
|
||||
}
|
||||
|
||||
iterations, err := strconv.Atoi(pieces[1])
|
||||
if err != nil {
|
||||
return HashedPassword{}, oops.New(err, "could not parse password iterations")
|
||||
}
|
||||
|
||||
return HashedPassword{
|
||||
Algorithm: HashAlgorithm(pieces[0]),
|
||||
Iterations: iterations,
|
||||
AlgoConfig: pieces[1],
|
||||
Salt: pieces[2],
|
||||
Hash: pieces[3],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p HashedPassword) String() string {
|
||||
return fmt.Sprintf("%s$%s$%s$%s", p.Algorithm, p.AlgoConfig, p.Salt, p.Hash)
|
||||
}
|
||||
|
||||
type Argon2idConfig struct {
|
||||
Time uint32
|
||||
Memory uint32
|
||||
Threads uint8
|
||||
KeyLength uint32
|
||||
}
|
||||
|
||||
func ParseArgon2idConfig(cfg string) (Argon2idConfig, error) {
|
||||
parts := strings.Split(cfg, ",")
|
||||
|
||||
t64, err := strconv.ParseUint(parts[0][2:], 10, 32)
|
||||
if err != nil {
|
||||
return Argon2idConfig{}, oops.New(err, "failed to parse time in Argon2id config")
|
||||
}
|
||||
|
||||
m64, err := strconv.ParseUint(parts[1][2:], 10, 32)
|
||||
if err != nil {
|
||||
return Argon2idConfig{}, oops.New(err, "failed to parse memory in Argon2id config")
|
||||
}
|
||||
|
||||
p64, err := strconv.ParseUint(parts[2][2:], 10, 8)
|
||||
if err != nil {
|
||||
return Argon2idConfig{}, oops.New(err, "failed to parse threads in Argon2id config")
|
||||
}
|
||||
|
||||
l64, err := strconv.ParseUint(parts[3][2:], 10, 32)
|
||||
if err != nil {
|
||||
return Argon2idConfig{}, oops.New(err, "failed to parse key length in Argon2id config")
|
||||
}
|
||||
|
||||
return Argon2idConfig{
|
||||
Time: uint32(t64),
|
||||
Memory: uint32(m64),
|
||||
Threads: uint8(p64),
|
||||
KeyLength: uint32(l64),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c Argon2idConfig) String() string {
|
||||
return fmt.Sprintf("t=%v,m=%v,p=%v,l=%v", c.Time, c.Memory, c.Threads, c.KeyLength)
|
||||
}
|
||||
|
||||
func CheckPassword(password string, hashedPassword HashedPassword) (bool, error) {
|
||||
switch hashedPassword.Algorithm {
|
||||
case PBKDF2_SHA256:
|
||||
case Argon2id:
|
||||
cfg, err := ParseArgon2idConfig(hashedPassword.AlgoConfig)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
salt, err := base64.StdEncoding.DecodeString(hashedPassword.Salt)
|
||||
if err != nil {
|
||||
return false, oops.New(err, "failed to decode salt")
|
||||
}
|
||||
|
||||
newHash := argon2.IDKey([]byte(password), []byte(salt), cfg.Time, cfg.Memory, cfg.Threads, cfg.KeyLength)
|
||||
newHashEnc := base64.StdEncoding.EncodeToString(newHash)
|
||||
|
||||
return bytes.Equal([]byte(newHashEnc), []byte(hashedPassword.Hash)), nil
|
||||
case Django_PBKDF2SHA256:
|
||||
decoded, err := base64.StdEncoding.DecodeString(hashedPassword.Hash)
|
||||
if err != nil {
|
||||
return false, oops.New(nil, "failed to get key length of hashed password")
|
||||
}
|
||||
|
||||
iterations, err := strconv.Atoi(hashedPassword.AlgoConfig)
|
||||
if err != nil {
|
||||
return false, oops.New(nil, "failed to get PBKDF2 iterations")
|
||||
}
|
||||
|
||||
newHash := pbkdf2.Key(
|
||||
[]byte(password),
|
||||
[]byte(hashedPassword.Salt),
|
||||
hashedPassword.Iterations,
|
||||
iterations,
|
||||
len(decoded),
|
||||
sha256.New,
|
||||
)
|
||||
|
@ -67,3 +138,32 @@ func CheckPassword(password string, hashedPassword HashedPassword) (bool, error)
|
|||
return false, oops.New(nil, "unrecognized password hash algorithm: %s", hashedPassword.Algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
func HashPassword(password string) (HashedPassword, error) {
|
||||
// Follows the OWASP recommendations as of March 2021.
|
||||
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
|
||||
|
||||
salt := make([]byte, saltLength)
|
||||
_, err := io.ReadFull(rand.Reader, salt)
|
||||
if err != nil {
|
||||
return HashedPassword{}, oops.New(err, "failed to generate salt")
|
||||
}
|
||||
saltEnc := base64.StdEncoding.EncodeToString(salt)
|
||||
|
||||
cfg := Argon2idConfig{
|
||||
Time: 1,
|
||||
Memory: 40 * 1024 * 1024,
|
||||
Threads: 1,
|
||||
KeyLength: keyLength,
|
||||
}
|
||||
|
||||
key := argon2.IDKey([]byte(password), salt, cfg.Time, cfg.Memory, cfg.Threads, cfg.KeyLength)
|
||||
keyEnc := base64.StdEncoding.EncodeToString(key)
|
||||
|
||||
return HashedPassword{
|
||||
Algorithm: Argon2id,
|
||||
AlgoConfig: cfg.String(),
|
||||
Salt: saltEnc,
|
||||
Hash: keyEnc,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
)
|
||||
|
||||
const AuthCookieName = "HMNToken"
|
||||
|
||||
type Token struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// TODO: ENCRYPT THIS
|
||||
|
||||
func EncodeToken(token Token) string {
|
||||
tokenBytes, _ := json.Marshal(token)
|
||||
return string(tokenBytes)
|
||||
}
|
||||
|
||||
func DecodeToken(tokenStr string) (Token, error) {
|
||||
var token Token
|
||||
err := json.Unmarshal([]byte(tokenStr), &token)
|
||||
if err != nil {
|
||||
// TODO: Is this worthy of an oops error, or should this just be a value handled silently by code?
|
||||
return Token{}, oops.New(err, "failed to unmarshal token")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func NewAuthCookie(username string) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: AuthCookieName,
|
||||
Value: EncodeToken(Token{
|
||||
Username: username,
|
||||
}),
|
||||
|
||||
Domain: config.Config.CookieDomain,
|
||||
// TODO: Path?
|
||||
|
||||
// Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteDefaultMode,
|
||||
}
|
||||
}
|
|
@ -15,10 +15,12 @@ const (
|
|||
)
|
||||
|
||||
type HMNConfig struct {
|
||||
Env Environment
|
||||
Addr string
|
||||
BaseUrl string
|
||||
Postgres PostgresConfig
|
||||
Env Environment
|
||||
Addr string
|
||||
BaseUrl string
|
||||
Postgres PostgresConfig
|
||||
CookieDomain string
|
||||
TokenSecret string
|
||||
}
|
||||
|
||||
type PostgresConfig struct {
|
||||
|
|
|
@ -3,7 +3,6 @@ package db
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
|
@ -61,8 +60,6 @@ func (it *StructQueryIterator) Next(dest interface{}) bool {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%#v\n", vals)
|
||||
|
||||
for i, val := range vals {
|
||||
field := v.Elem().Field(it.fieldIndices[i])
|
||||
switch field.Kind() {
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
<header class="mb3">
|
||||
{{/* TODO */}}
|
||||
{{/* <div class="user-options flex justify-center justify-end-ns">
|
||||
{% if user.is_authenticated %}
|
||||
{% if user.is_superuser %}
|
||||
<a class="admin-panel" href="{% url 'admin_panel' subdomain=None %}"><span class="icon-settings"> Admin</span></a>
|
||||
{% endif %}
|
||||
<a class="username settings" href="{% url 'member_settings' subdomain=None %}"><span class="icon-settings"></span> {{user.username}}</a>
|
||||
<a class="logout" href="{% url 'member_logout' %}"><span class="icon-logout"></span> Logout</a>
|
||||
{% else %}
|
||||
<a class="register" id="register-link" href="{% url 'member_register' subdomain=None %}">Register</a>
|
||||
<a class="login" id="login-link" href="{% url 'member_login' subdomain=request.subdomain %}">Log in</a>
|
||||
{{/* TODO: All the URLs in here are wrong. */}}
|
||||
<div class="user-options flex justify-center justify-end-ns">
|
||||
{{ if .User }}
|
||||
{{ if .User.IsSuperuser }}
|
||||
<a class="admin-panel" href="{{ url "/admin_panel" }}"><span class="icon-settings"> Admin</span></a>
|
||||
{{ end }}
|
||||
<a class="username settings" href="{{ url "/member_settings" }}"><span class="icon-settings"></span> {{ .User.Username }}</a>
|
||||
<a class="logout" href="{{ url "/member_logout" }}"><span class="icon-logout"></span> Logout</a>
|
||||
{{ else }}
|
||||
<a class="register" id="register-link" href="{{ url "/member_register" }}">Register</a>
|
||||
<a class="login" id="login-link" href="{{ projecturl "/login" }}">Log in</a>
|
||||
<div id="login-popup">
|
||||
<form action="{% url 'member_login' subdomain=request.subdomain %}" method="post">
|
||||
{% csrf_token %}
|
||||
<form action="{{ projecturl "/login" }}" method="post">
|
||||
{{/* TODO: CSRF */}}
|
||||
<table>
|
||||
{% if error_message %}
|
||||
<th colspan="2">{{ error_message }}</th>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th><label>Username:</label></th>
|
||||
<td><input type="text" name="username" class="textbox username" value="" /></td>
|
||||
|
@ -25,17 +22,15 @@
|
|||
<th><label>Password:</label></th>
|
||||
<td><input type="password" name="password" class="textbox password" value="" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
{# <td colspan="2"><a href="#">Forgot your password?</a></td> #}
|
||||
</tr>
|
||||
{{/* TODO: Forgot password flow? Or just on standalone page? */}}
|
||||
</table>
|
||||
<div class="actionbar pt2">
|
||||
<input type="submit" value="Log In" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div> */}}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="menu-bar flex flex-column flex-row-l justify-between {% if project and project.slug != 'hmn' %}project{% endif %}">
|
||||
<div class="flex flex-column flex-row-ns">
|
||||
<a href="{{ url "/" }}" class="logo hmdev-logo">
|
||||
|
@ -72,8 +67,8 @@
|
|||
{{ end }} */}}
|
||||
</div>
|
||||
</div>
|
||||
<form class="dn ma0 flex-l flex-column justify-center items-end" method="post" action="{% url 'search' %}" target="_blank">
|
||||
{% csrf_token %}
|
||||
<form class="dn ma0 flex-l flex-column justify-center items-end" method="post" action="{{ url "/search" }}" target="_blank">
|
||||
{{/* TODO: CSRF? */}}
|
||||
<input class="site-search bn lite pa2 fira" type="text" name="term" value="" placeholder="Search with Google" size="17" />
|
||||
<input id="search_button_homepage" type="submit" value="Go"/>
|
||||
</form>
|
||||
|
|
|
@ -49,10 +49,6 @@ func Init() {
|
|||
Templates[f.Name()] = t
|
||||
}
|
||||
}
|
||||
|
||||
for name, t := range Templates {
|
||||
fmt.Printf("%s: %v\n", name, names(t.Templates()))
|
||||
}
|
||||
}
|
||||
|
||||
func names(ts []*template.Template) []string {
|
||||
|
|
|
@ -5,9 +5,11 @@ type BaseData struct {
|
|||
CanonicalLink string
|
||||
OpenGraphItems []OpenGraphItem
|
||||
BackgroundImage BackgroundImage
|
||||
Project Project
|
||||
Theme string
|
||||
BodyClasses []string
|
||||
|
||||
Project Project
|
||||
User *User
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
|
@ -23,6 +25,13 @@ type Project struct {
|
|||
HasLibrary bool
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Email string
|
||||
IsSuperuser bool
|
||||
IsStaff bool
|
||||
}
|
||||
|
||||
type OpenGraphItem struct {
|
||||
Property string
|
||||
Name string
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package website
|
||||
|
||||
import "fmt"
|
||||
|
||||
// A SafeError can be used to wrap another error and explicitly provide
|
||||
// an error message that is safe to show to a user. This allows the original
|
||||
// error to easily be logged and for servers to consistently return errors
|
||||
// in a standard format, without having to worry about leaking sensitive
|
||||
// info (assuming you use the right middleware!).
|
||||
type SafeError struct {
|
||||
Wrapped error
|
||||
Msg string
|
||||
}
|
||||
|
||||
func NewSafeError(err error, msg string, args ...interface{}) error {
|
||||
return &SafeError{
|
||||
Wrapped: err,
|
||||
Msg: fmt.Sprintf(msg, args...),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SafeError) Error() string {
|
||||
return s.Msg
|
||||
}
|
|
@ -3,9 +3,13 @@ package website
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/logging"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
|
@ -90,6 +94,80 @@ func (c *RequestContext) Headers() http.Header {
|
|||
return c.rw.Header()
|
||||
}
|
||||
|
||||
func (c *RequestContext) SetCookie(cookie *http.Cookie) {
|
||||
c.rw.Header().Add("Set-Cookie", cookie.String())
|
||||
}
|
||||
|
||||
func (c *RequestContext) GetFormValues() (url.Values, error) {
|
||||
err := c.Req.ParseForm()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.Req.PostForm, nil
|
||||
}
|
||||
|
||||
// The logic of this function is copy-pasted from the Go standard library.
|
||||
// https://golang.org/pkg/net/http/#Redirect
|
||||
func (c *RequestContext) Redirect(dest string, code int) {
|
||||
if u, err := url.Parse(dest); err == nil {
|
||||
// If url was relative, make its path absolute by
|
||||
// combining with request path.
|
||||
// The client would probably do this for us,
|
||||
// but doing it ourselves is more reliable.
|
||||
// See RFC 7231, section 7.1.2
|
||||
if u.Scheme == "" && u.Host == "" {
|
||||
oldpath := c.Req.URL.Path
|
||||
if oldpath == "" { // should not happen, but avoid a crash if it does
|
||||
oldpath = "/"
|
||||
}
|
||||
|
||||
// no leading http://server
|
||||
if dest == "" || dest[0] != '/' {
|
||||
// make relative path absolute
|
||||
olddir, _ := path.Split(oldpath)
|
||||
dest = olddir + dest
|
||||
}
|
||||
|
||||
var query string
|
||||
if i := strings.Index(dest, "?"); i != -1 {
|
||||
dest, query = dest[:i], dest[i:]
|
||||
}
|
||||
|
||||
// clean up but preserve trailing slash
|
||||
trailing := strings.HasSuffix(dest, "/")
|
||||
dest = path.Clean(dest)
|
||||
if trailing && !strings.HasSuffix(dest, "/") {
|
||||
dest += "/"
|
||||
}
|
||||
dest += query
|
||||
}
|
||||
}
|
||||
|
||||
h := c.Headers()
|
||||
|
||||
// RFC 7231 notes that a short HTML body is usually included in
|
||||
// the response because older user agents may not understand 301/307.
|
||||
// Do it only if the request didn't already have a Content-Type header.
|
||||
_, hadCT := h["Content-Type"]
|
||||
|
||||
// Escape stuff
|
||||
destUrl, _ := url.Parse(dest)
|
||||
dest = destUrl.String()
|
||||
|
||||
h.Set("Location", dest)
|
||||
if !hadCT && (c.Req.Method == "GET" || c.Req.Method == "HEAD") {
|
||||
h.Set("Content-Type", "text/html; charset=utf-8")
|
||||
}
|
||||
c.StatusCode = code
|
||||
|
||||
// Shouldn't send the body for POST or HEAD; that leaves GET.
|
||||
if !hadCT && c.Req.Method == "GET" {
|
||||
body := "<a href=\"" + html.EscapeString(dest) + "\">" + http.StatusText(code) + "</a>.\n"
|
||||
fmt.Fprintln(c.Body, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RequestContext) WriteTemplate(name string, data interface{}) error {
|
||||
return templates.Templates[name].Execute(c.Body, data)
|
||||
}
|
||||
|
|
|
@ -2,10 +2,8 @@ package website
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
|
@ -130,21 +128,27 @@ func (s *websiteRoutes) ProjectCSS(c *RequestContext, p httprouter.Params) {
|
|||
}
|
||||
|
||||
func (s *websiteRoutes) Login(c *RequestContext, p httprouter.Params) {
|
||||
bodyBytes, _ := io.ReadAll(c.Req.Body)
|
||||
|
||||
// TODO: Update this endpoint to give uniform responses on errors and to be resilient to timing attacks.
|
||||
|
||||
var body struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
err := json.Unmarshal(bodyBytes, &body)
|
||||
form, err := c.GetFormValues()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
c.Errored(http.StatusBadRequest, NewSafeError(err, "request must contain form data"))
|
||||
return
|
||||
}
|
||||
|
||||
username := form.Get("username")
|
||||
password := form.Get("password")
|
||||
if username == "" || password == "" {
|
||||
c.Errored(http.StatusBadRequest, NewSafeError(err, "you must provide both a username and password"))
|
||||
}
|
||||
|
||||
redirect := form.Get("redirect")
|
||||
if redirect == "" {
|
||||
redirect = "/"
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err = db.QueryOneToStruct(c.Context(), s.conn, &user, "SELECT $columns FROM auth_user WHERE username = $1", body.Username)
|
||||
err = db.QueryOneToStruct(c.Context(), s.conn, &user, "SELECT $columns FROM auth_user WHERE username = $1", username)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoMatchingRows) {
|
||||
c.StatusCode = http.StatusUnauthorized
|
||||
|
@ -154,25 +158,26 @@ func (s *websiteRoutes) Login(c *RequestContext, p httprouter.Params) {
|
|||
return
|
||||
}
|
||||
|
||||
logging.Debug().Interface("user", user).Msg("the user to check")
|
||||
|
||||
hashed, err := auth.ParseDjangoPasswordString(user.Password)
|
||||
hashed, err := auth.ParsePasswordString(user.Password)
|
||||
if err != nil {
|
||||
c.Errored(http.StatusInternalServerError, oops.New(err, "failed to parse password string"))
|
||||
return
|
||||
}
|
||||
|
||||
passwordsMatch, err := auth.CheckPassword(body.Password, hashed)
|
||||
passwordsMatch, err := auth.CheckPassword(password, hashed)
|
||||
if err != nil {
|
||||
c.Errored(http.StatusInternalServerError, oops.New(err, "failed to check password against hash"))
|
||||
return
|
||||
}
|
||||
|
||||
if passwordsMatch {
|
||||
c.Body.WriteString("ur good")
|
||||
logging.Debug().Str("cookie", auth.NewAuthCookie(username).String()).Msg("logged in")
|
||||
c.SetCookie(auth.NewAuthCookie(username))
|
||||
c.Redirect(redirect, http.StatusSeeOther)
|
||||
return
|
||||
} else {
|
||||
c.StatusCode = http.StatusUnauthorized
|
||||
c.Body.WriteString("nope")
|
||||
c.Redirect("/", http.StatusSeeOther) // TODO: Redirect to standalone login page with error
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue