mirror of
https://github.com/1f349/tulip.git
synced 2024-11-12 14:51:32 +00:00
Replace session with single login cookie, just use strings for user ids
This commit is contained in:
parent
e822172513
commit
552ec72ded
@ -5,14 +5,13 @@ import (
|
||||
"fmt"
|
||||
"github.com/MrMelon54/pronouns"
|
||||
"github.com/go-oauth2/oauth2/v4"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/text/language"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Sub uuid.UUID `json:"sub"`
|
||||
Sub string `json:"sub"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Username string `json:"username"`
|
||||
Picture NullStringScanner `json:"picture,omitempty"`
|
||||
|
@ -59,37 +59,37 @@ func (t *Tx) CheckLogin(un, pw string) (*User, bool, bool, error) {
|
||||
return &u, hasOtp, hasVerify, err
|
||||
}
|
||||
|
||||
func (t *Tx) GetUserDisplayName(sub uuid.UUID) (*User, error) {
|
||||
func (t *Tx) GetUserDisplayName(sub string) (*User, error) {
|
||||
var u User
|
||||
row := t.tx.QueryRow(`SELECT name FROM users WHERE subject = ? LIMIT 1`, sub.String())
|
||||
row := t.tx.QueryRow(`SELECT name FROM users WHERE subject = ? LIMIT 1`, sub)
|
||||
err := row.Scan(&u.Name)
|
||||
u.Sub = sub
|
||||
return &u, err
|
||||
}
|
||||
|
||||
func (t *Tx) GetUserRole(sub uuid.UUID) (UserRole, error) {
|
||||
func (t *Tx) GetUserRole(sub string) (UserRole, error) {
|
||||
var r UserRole
|
||||
row := t.tx.QueryRow(`SELECT role FROM users WHERE subject = ? LIMIT 1`, sub.String())
|
||||
row := t.tx.QueryRow(`SELECT role FROM users WHERE subject = ? LIMIT 1`, sub)
|
||||
err := row.Scan(&r)
|
||||
return r, err
|
||||
}
|
||||
|
||||
func (t *Tx) GetUser(sub uuid.UUID) (*User, error) {
|
||||
func (t *Tx) GetUser(sub string) (*User, error) {
|
||||
var u User
|
||||
row := t.tx.QueryRow(`SELECT name, username, picture, website, email, email_verified, pronouns, birthdate, zoneinfo, locale, updated_at, active FROM users WHERE subject = ?`, sub.String())
|
||||
row := t.tx.QueryRow(`SELECT name, username, picture, website, email, email_verified, pronouns, birthdate, zoneinfo, locale, updated_at, active FROM users WHERE subject = ?`, sub)
|
||||
err := row.Scan(&u.Name, &u.Username, &u.Picture, &u.Website, &u.Email, &u.EmailVerified, &u.Pronouns, &u.Birthdate, &u.ZoneInfo, &u.Locale, &u.UpdatedAt, &u.Active)
|
||||
u.Sub = sub
|
||||
return &u, err
|
||||
}
|
||||
|
||||
func (t *Tx) GetUserEmail(sub uuid.UUID) (string, error) {
|
||||
func (t *Tx) GetUserEmail(sub string) (string, error) {
|
||||
var email string
|
||||
row := t.tx.QueryRow(`SELECT email FROM users WHERE subject = ?`, sub.String())
|
||||
row := t.tx.QueryRow(`SELECT email FROM users WHERE subject = ?`, sub)
|
||||
err := row.Scan(&email)
|
||||
return email, err
|
||||
}
|
||||
|
||||
func (t *Tx) ChangeUserPassword(sub uuid.UUID, pwOld, pwNew string) error {
|
||||
func (t *Tx) ChangeUserPassword(sub, pwOld, pwNew string) error {
|
||||
q, err := t.tx.Query(`SELECT password FROM users WHERE subject = ?`, sub)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -131,7 +131,7 @@ func (t *Tx) ChangeUserPassword(sub uuid.UUID, pwOld, pwNew string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tx) ModifyUser(sub uuid.UUID, v *UserPatch) error {
|
||||
func (t *Tx) ModifyUser(sub string, v *UserPatch) error {
|
||||
exec, err := t.tx.Exec(
|
||||
`UPDATE users
|
||||
SET name = ?,
|
||||
@ -166,19 +166,19 @@ WHERE subject = ?`,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tx) SetTwoFactor(sub uuid.UUID, secret string, digits int) error {
|
||||
func (t *Tx) SetTwoFactor(sub string, secret string, digits int) error {
|
||||
if secret == "" && digits == 0 {
|
||||
_, err := t.tx.Exec(`DELETE FROM otp WHERE otp.subject = ?`, sub.String())
|
||||
_, err := t.tx.Exec(`DELETE FROM otp WHERE otp.subject = ?`, sub)
|
||||
return err
|
||||
}
|
||||
_, err := t.tx.Exec(`INSERT INTO otp(subject, secret, digits) VALUES (?, ?, ?) ON CONFLICT(subject) DO UPDATE SET secret = excluded.secret, digits = excluded.digits`, sub.String(), secret, digits)
|
||||
_, err := t.tx.Exec(`INSERT INTO otp(subject, secret, digits) VALUES (?, ?, ?) ON CONFLICT(subject) DO UPDATE SET secret = excluded.secret, digits = excluded.digits`, sub, secret, digits)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Tx) GetTwoFactor(sub uuid.UUID) (string, int, error) {
|
||||
func (t *Tx) GetTwoFactor(sub string) (string, int, error) {
|
||||
var secret string
|
||||
var digits int
|
||||
row := t.tx.QueryRow(`SELECT secret, digits FROM otp WHERE subject = ?`, sub.String())
|
||||
row := t.tx.QueryRow(`SELECT secret, digits FROM otp WHERE subject = ?`, sub)
|
||||
err := row.Scan(&secret, &digits)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
@ -186,7 +186,7 @@ func (t *Tx) GetTwoFactor(sub uuid.UUID) (string, int, error) {
|
||||
return secret, digits, nil
|
||||
}
|
||||
|
||||
func (t *Tx) HasTwoFactor(sub uuid.UUID) (bool, error) {
|
||||
func (t *Tx) HasTwoFactor(sub string) (bool, error) {
|
||||
var hasOtp bool
|
||||
row := t.tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM otp WHERE otp.subject = ?)`, sub)
|
||||
err := row.Scan(&hasOtp)
|
||||
@ -207,9 +207,9 @@ func (t *Tx) GetClientInfo(sub string) (oauth2.ClientInfo, error) {
|
||||
return &u, err
|
||||
}
|
||||
|
||||
func (t *Tx) GetAppList(owner uuid.UUID, admin bool, offset int) ([]ClientInfoDbOutput, error) {
|
||||
func (t *Tx) GetAppList(owner string, admin bool, offset int) ([]ClientInfoDbOutput, error) {
|
||||
var u []ClientInfoDbOutput
|
||||
row, err := t.tx.Query(`SELECT subject, name, domain, owner, public, sso, active FROM client_store WHERE owner = ? OR ? = 1 LIMIT 25 OFFSET ?`, owner.String(), admin, offset)
|
||||
row, err := t.tx.Query(`SELECT subject, name, domain, owner, public, sso, active FROM client_store WHERE owner = ? OR ? = 1 LIMIT 25 OFFSET ?`, owner, admin, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -225,27 +225,27 @@ func (t *Tx) GetAppList(owner uuid.UUID, admin bool, offset int) ([]ClientInfoDb
|
||||
return u, row.Err()
|
||||
}
|
||||
|
||||
func (t *Tx) InsertClientApp(name, domain string, public, sso, active bool, owner uuid.UUID) error {
|
||||
func (t *Tx) InsertClientApp(name, domain string, public, sso, active bool, owner string) error {
|
||||
u := uuid.New()
|
||||
secret, err := password.GenerateApiSecret(70)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = t.tx.Exec(`INSERT INTO client_store (subject, name, secret, domain, owner, public, sso, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, u.String(), name, secret, domain, owner.String(), public, sso, active)
|
||||
_, err = t.tx.Exec(`INSERT INTO client_store (subject, name, secret, domain, owner, public, sso, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, u.String(), name, secret, domain, owner, public, sso, active)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Tx) UpdateClientApp(subject, owner uuid.UUID, name, domain string, public, sso, active bool) error {
|
||||
_, err := t.tx.Exec(`UPDATE client_store SET name = ?, domain = ?, public = ?, sso = ?, active = ? WHERE subject = ? AND owner = ?`, name, domain, public, sso, active, subject.String(), owner.String())
|
||||
func (t *Tx) UpdateClientApp(subject, owner string, name, domain string, public, sso, active bool) error {
|
||||
_, err := t.tx.Exec(`UPDATE client_store SET name = ?, domain = ?, public = ?, sso = ?, active = ? WHERE subject = ? AND owner = ?`, name, domain, public, sso, active, subject, owner)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Tx) ResetClientAppSecret(subject, owner uuid.UUID) (string, error) {
|
||||
func (t *Tx) ResetClientAppSecret(subject, owner string) (string, error) {
|
||||
secret, err := password.GenerateApiSecret(70)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = t.tx.Exec(`UPDATE client_store SET secret = ? WHERE subject = ? AND owner = ?`, secret, subject.String(), owner.String())
|
||||
_, err = t.tx.Exec(`UPDATE client_store SET secret = ? WHERE subject = ? AND owner = ?`, secret, subject, owner)
|
||||
return secret, err
|
||||
}
|
||||
|
||||
@ -266,22 +266,22 @@ func (t *Tx) GetUserList(offset int) ([]User, error) {
|
||||
return u, row.Err()
|
||||
}
|
||||
|
||||
func (t *Tx) UpdateUser(subject uuid.UUID, role UserRole, active bool) error {
|
||||
func (t *Tx) UpdateUser(subject string, role UserRole, active bool) error {
|
||||
_, err := t.tx.Exec(`UPDATE users SET active = ?, role = ? WHERE subject = ?`, active, role, subject)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Tx) VerifyUserEmail(sub uuid.UUID) error {
|
||||
_, err := t.tx.Exec(`UPDATE users SET email_verified = 1 WHERE subject = ?`, sub.String())
|
||||
func (t *Tx) VerifyUserEmail(sub string) error {
|
||||
_, err := t.tx.Exec(`UPDATE users SET email_verified = 1 WHERE subject = ?`, sub)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Tx) UserResetPassword(sub uuid.UUID, pw string) error {
|
||||
func (t *Tx) UserResetPassword(sub string, pw string) error {
|
||||
hashPassword, err := password.HashPassword(pw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exec, err := t.tx.Exec(`UPDATE users SET password = ?, updated_at = ? WHERE subject = ?`, hashPassword, updatedAt(), sub.String())
|
||||
exec, err := t.tx.Exec(`UPDATE users SET password = ?, updated_at = ? WHERE subject = ?`, hashPassword, updatedAt(), sub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ func TestTx_ChangeUserPassword(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
tx, err := d.Begin()
|
||||
assert.NoError(t, err)
|
||||
err = tx.ChangeUserPassword(u, "test", "new")
|
||||
err = tx.ChangeUserPassword(u.String(), "test", "new")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, tx.Commit())
|
||||
query, err := d.db.Query(`SELECT password FROM users WHERE subject = ? AND username = ?`, u.String(), "test")
|
||||
@ -43,7 +43,7 @@ func TestTx_ModifyUser(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
tx, err := d.Begin()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, tx.ModifyUser(u, &UserPatch{
|
||||
assert.NoError(t, tx.ModifyUser(u.String(), &UserPatch{
|
||||
Name: "example",
|
||||
Pronouns: pronouns.TheyThem,
|
||||
ZoneInfo: time.UTC,
|
||||
|
9
go.mod
9
go.mod
@ -4,7 +4,7 @@ go 1.22
|
||||
|
||||
require (
|
||||
github.com/1f349/cache v0.0.2
|
||||
github.com/1f349/mjwt v0.2.1
|
||||
github.com/1f349/mjwt v0.2.2
|
||||
github.com/1f349/overlapfs v0.0.1
|
||||
github.com/1f349/violet v0.0.13
|
||||
github.com/MrMelon54/exit-reload v0.0.1
|
||||
@ -14,6 +14,7 @@ require (
|
||||
github.com/emersion/go-smtp v0.20.2
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2
|
||||
github.com/go-session/session v3.1.2+incompatible
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/google/subcommands v1.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
@ -21,16 +22,16 @@ require (
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
golang.org/x/crypto v0.18.0
|
||||
golang.org/x/crypto v0.19.0
|
||||
golang.org/x/text v0.14.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/MrMelon54/rescheduler v0.0.2 // indirect
|
||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tidwall/btree v1.7.0 // indirect
|
||||
@ -41,6 +42,6 @@ require (
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/rtred v0.1.2 // indirect
|
||||
github.com/tidwall/tinyqueue v0.1.1 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
14
go.sum
14
go.sum
@ -1,8 +1,8 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/1f349/cache v0.0.2 h1:27QD6zPd9xYyvh9V1qqWq+EAt5+N+qvyGWKfnjMrhP8=
|
||||
github.com/1f349/cache v0.0.2/go.mod h1:LibAMy13dF0KO1fQA9aEjZPBCB6Y4b5kKYEQJUqc2rQ=
|
||||
github.com/1f349/mjwt v0.2.1 h1:REdiM/MaNjYQwHvI39LaMPhlvMg4Vy9SgomWMsKTNz8=
|
||||
github.com/1f349/mjwt v0.2.1/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU=
|
||||
github.com/1f349/mjwt v0.2.2 h1:mVw71zcf0D7dWgZXMvjXMq8oNn41V1DFyLY0Ywkq1VQ=
|
||||
github.com/1f349/mjwt v0.2.2/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU=
|
||||
github.com/1f349/overlapfs v0.0.1 h1:LAxBolrXFAgU0yqZtXg/C/aaPq3eoQSPpBc49BHuTp0=
|
||||
github.com/1f349/overlapfs v0.0.1/go.mod h1:I6aItQycr7nrzplmfNXp/QF9tTmKRSgY3fXmu/7Ky2o=
|
||||
github.com/1f349/violet v0.0.13 h1:lJpTz15Ea83Uc1VAISXTjtKuzr8Pe8NM4cMGp3Aiyhk=
|
||||
@ -17,6 +17,8 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
|
||||
github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@ -171,8 +173,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -187,8 +189,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -10,21 +10,14 @@
|
||||
</header>
|
||||
<main>
|
||||
<form method="POST" action="/mail/password">
|
||||
<input type="hidden" name="code" value="{{.Code}}"/>
|
||||
<div>
|
||||
<label for="field_new_password">New Password:</label>
|
||||
<input type="password"
|
||||
name="new_password"
|
||||
id="field_new_password"
|
||||
autocomplete="new_password"
|
||||
required/>
|
||||
<input type="password" name="new_password" id="field_new_password" autocomplete="new_password" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_confirm_password">Confirm Password:</label>
|
||||
<input type="password"
|
||||
name="confirm_password"
|
||||
id="field_confirm_password"
|
||||
autocomplete="confirm_password"
|
||||
required/>
|
||||
<input type="password" name="confirm_password" id="field_confirm_password" autocomplete="confirm_password" required/>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
|
@ -1,14 +1,9 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/mjwt/auth"
|
||||
"github.com/1f349/tulip/database"
|
||||
"github.com/go-session/session"
|
||||
"github.com/google/uuid"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -18,36 +13,26 @@ import (
|
||||
type UserHandler func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth)
|
||||
|
||||
type UserAuth struct {
|
||||
Session session.Store
|
||||
Data SessionData
|
||||
}
|
||||
|
||||
type SessionData struct {
|
||||
ID uuid.UUID
|
||||
ID string
|
||||
NeedOtp bool
|
||||
}
|
||||
|
||||
func (u UserAuth) NextFlowUrl(origin *url.URL) *url.URL {
|
||||
if u.Data.NeedOtp {
|
||||
if u.NeedOtp {
|
||||
return PrepareRedirectUrl("/login/otp", origin)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u UserAuth) IsGuest() bool {
|
||||
return u.Data.ID == uuid.Nil
|
||||
}
|
||||
|
||||
func (u UserAuth) SaveSessionData() error {
|
||||
u.Session.Set("session-data", u.Data)
|
||||
return u.Session.Save()
|
||||
return u.ID == ""
|
||||
}
|
||||
|
||||
func (h *HttpServer) RequireAdminAuthentication(next UserHandler) httprouter.Handle {
|
||||
return h.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
|
||||
var role database.UserRole
|
||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||
role, err = tx.GetUserRole(auth.Data.ID)
|
||||
role, err = tx.GetUserRole(auth.ID)
|
||||
return
|
||||
}) {
|
||||
return
|
||||
@ -73,54 +58,27 @@ func (h *HttpServer) RequireAuthentication(next UserHandler) httprouter.Handle {
|
||||
|
||||
func (h *HttpServer) OptionalAuthentication(flowPart bool, next UserHandler) httprouter.Handle {
|
||||
return func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
auth, err := internalAuthenticationHandler(rw, req)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if n := auth.NextFlowUrl(req.URL); n != nil && !flowPart {
|
||||
http.Redirect(rw, req, n.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
if auth.IsGuest() {
|
||||
if loginCookie, err := req.Cookie("tulip-login-data"); err == nil {
|
||||
if decryptedBytes, err := base64.RawStdEncoding.DecodeString(loginCookie.Value); err == nil {
|
||||
if decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, h.signingKey.PrivateKey(), decryptedBytes, []byte("tulip-login-data")); err == nil {
|
||||
if len(decryptedData) == 16 {
|
||||
var u uuid.UUID
|
||||
copy(u[:], decryptedData[:])
|
||||
auth.Data.ID = u
|
||||
auth.Data.NeedOtp = false
|
||||
}
|
||||
}
|
||||
}
|
||||
authData, err := h.internalAuthenticationHandler(req)
|
||||
if err == nil {
|
||||
if n := authData.NextFlowUrl(req.URL); n != nil && !flowPart {
|
||||
http.Redirect(rw, req, n.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
next(rw, req, params, auth)
|
||||
next(rw, req, params, authData)
|
||||
}
|
||||
}
|
||||
|
||||
func internalAuthenticationHandler(rw http.ResponseWriter, req *http.Request) (UserAuth, error) {
|
||||
ss, err := session.Start(req.Context(), rw, req)
|
||||
if err != nil {
|
||||
return UserAuth{}, fmt.Errorf("failed to start session")
|
||||
}
|
||||
|
||||
// get auth object
|
||||
userIdRaw, ok := ss.Get("session-data")
|
||||
if !ok {
|
||||
return UserAuth{Session: ss}, nil
|
||||
}
|
||||
userData, ok := userIdRaw.(SessionData)
|
||||
if !ok {
|
||||
ss.Delete("session-data")
|
||||
err := ss.Save()
|
||||
func (h *HttpServer) internalAuthenticationHandler(req *http.Request) (UserAuth, error) {
|
||||
if loginCookie, err := req.Cookie("tulip-login-data"); err == nil {
|
||||
_, b, err := mjwt.ExtractClaims[auth.AccessTokenClaims](h.signingKey, loginCookie.Value)
|
||||
if err != nil {
|
||||
return UserAuth{Session: ss}, fmt.Errorf("failed to reset invalid session data")
|
||||
return UserAuth{}, err
|
||||
}
|
||||
return UserAuth{ID: b.Subject, NeedOtp: b.Claims.Perms.Has("need-otp")}, nil
|
||||
}
|
||||
|
||||
return UserAuth{Session: ss, Data: userData}, nil
|
||||
// not logged in
|
||||
return UserAuth{}, nil
|
||||
}
|
||||
|
||||
func PrepareRedirectUrl(targetPath string, origin *url.URL) *url.URL {
|
||||
|
@ -2,28 +2,26 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUserAuth_NextFlowUrl(t *testing.T) {
|
||||
u := UserAuth{Data: SessionData{NeedOtp: true}}
|
||||
u := UserAuth{NeedOtp: true}
|
||||
assert.Equal(t, url.URL{Path: "/login/otp"}, *u.NextFlowUrl(&url.URL{}))
|
||||
assert.Equal(t, url.URL{Path: "/login/otp", RawQuery: url.Values{"redirect": {"/hello"}}.Encode()}, *u.NextFlowUrl(&url.URL{Path: "/hello"}))
|
||||
assert.Equal(t, url.URL{Path: "/login/otp", RawQuery: url.Values{"redirect": {"/hello?a=A"}}.Encode()}, *u.NextFlowUrl(&url.URL{Path: "/hello", RawQuery: url.Values{"a": {"A"}}.Encode()}))
|
||||
u.Data.NeedOtp = false
|
||||
u.NeedOtp = false
|
||||
assert.Nil(t, u.NextFlowUrl(&url.URL{}))
|
||||
}
|
||||
|
||||
func TestUserAuth_IsGuest(t *testing.T) {
|
||||
var u UserAuth
|
||||
assert.True(t, u.IsGuest())
|
||||
u.Data.ID = uuid.New()
|
||||
u.ID = uuid.NewString()
|
||||
assert.False(t, u.IsGuest())
|
||||
}
|
||||
|
||||
@ -42,70 +40,17 @@ func (f *fakeSessionStore) Get(key string) (a interface{}, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (f *fakeSessionStore) Delete(key string) (i interface{}) {
|
||||
i = f.m[key]
|
||||
delete(f.m, key)
|
||||
return
|
||||
}
|
||||
|
||||
func (f *fakeSessionStore) Save() error {
|
||||
return f.saveFunc(f.m)
|
||||
}
|
||||
|
||||
func (f *fakeSessionStore) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestUserAuth_SaveSessionData(t *testing.T) {
|
||||
f := &fakeSessionStore{m: make(map[string]any)}
|
||||
u := UserAuth{Data: SessionData{ID: uuid.UUID{5, 6, 7}, NeedOtp: true}, Session: f}
|
||||
|
||||
// fail to save
|
||||
f.saveFunc = func(m map[string]any) error { return fmt.Errorf("failed") }
|
||||
assert.Error(t, u.SaveSessionData())
|
||||
|
||||
// try with success
|
||||
var m2 map[string]any
|
||||
f.saveFunc = func(m map[string]any) error {
|
||||
m2 = m
|
||||
return nil
|
||||
}
|
||||
assert.NoError(t, u.SaveSessionData())
|
||||
assert.Equal(t, map[string]any{"session-data": SessionData{ID: uuid.UUID{5, 6, 7}, NeedOtp: true}}, m2)
|
||||
}
|
||||
|
||||
func TestRequireAuthentication(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOptionalAuthentication(t *testing.T) {
|
||||
h := &HttpServer{}
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com/hello", nil)
|
||||
assert.NoError(t, err)
|
||||
rec := httptest.NewRecorder()
|
||||
auth, err := internalAuthenticationHandler(rec, req)
|
||||
auth, err := h.internalAuthenticationHandler(req)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, auth.IsGuest())
|
||||
auth.Data.ID = uuid.UUID{5, 6, 7}
|
||||
assert.NoError(t, auth.SaveSessionData())
|
||||
}
|
||||
|
||||
func Test_internalAuthenticationHandler(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com/hello", nil)
|
||||
assert.NoError(t, err)
|
||||
rec := httptest.NewRecorder()
|
||||
auth, err := internalAuthenticationHandler(rec, req)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, auth.IsGuest())
|
||||
auth.Data.ID = uuid.UUID{5, 6, 7}
|
||||
assert.NoError(t, auth.SaveSessionData())
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, "https://example.com/world", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Cookie", rec.Header().Get("Set-Cookie"))
|
||||
rec = httptest.NewRecorder()
|
||||
auth, err = internalAuthenticationHandler(rec, req)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, auth.IsGuest())
|
||||
assert.Equal(t, uuid.UUID{5, 6, 7}, auth.Data.ID)
|
||||
auth.ID = "567"
|
||||
}
|
||||
|
||||
func TestPrepareRedirectUrl(t *testing.T) {
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (h *HttpServer) EditGet(rw http.ResponseWriter, _ *http.Request, _ httprouter.Params, auth UserAuth) {
|
||||
@ -15,7 +16,7 @@ func (h *HttpServer) EditGet(rw http.ResponseWriter, _ *http.Request, _ httprout
|
||||
|
||||
if h.DbTx(rw, func(tx *database.Tx) error {
|
||||
var err error
|
||||
user, err = tx.GetUser(auth.Data.ID)
|
||||
user, err = tx.GetUser(auth.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read user data: %w", err)
|
||||
}
|
||||
@ -25,11 +26,14 @@ func (h *HttpServer) EditGet(rw http.ResponseWriter, _ *http.Request, _ httprout
|
||||
}
|
||||
|
||||
lNonce := uuid.NewString()
|
||||
auth.Session.Set("action-nonce", lNonce)
|
||||
if auth.Session.Save() != nil {
|
||||
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "tulip-nonce",
|
||||
Value: lNonce,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(10 * time.Minute),
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
pages.RenderPageTemplate(rw, "edit", map[string]any{
|
||||
"ServiceName": h.conf.ServiceName,
|
||||
"User": user,
|
||||
@ -61,7 +65,7 @@ func (h *HttpServer) EditPost(rw http.ResponseWriter, req *http.Request, _ httpr
|
||||
return
|
||||
}
|
||||
if h.DbTx(rw, func(tx *database.Tx) error {
|
||||
if err := tx.ModifyUser(auth.Data.ID, &patch); err != nil {
|
||||
if err := tx.ModifyUser(auth.ID, &patch); err != nil {
|
||||
return fmt.Errorf("failed to modify user info: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
@ -7,11 +7,21 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {
|
||||
rw.Header().Set("Content-Type", "text/html")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
lNonce := uuid.NewString()
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "tulip-nonce",
|
||||
Value: lNonce,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(10 * time.Minute),
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
|
||||
if auth.IsGuest() {
|
||||
pages.RenderPageTemplate(rw, "index-guest", map[string]any{
|
||||
"ServiceName": h.conf.ServiceName,
|
||||
@ -19,24 +29,21 @@ func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httproute
|
||||
return
|
||||
}
|
||||
|
||||
lNonce := uuid.NewString()
|
||||
auth.Session.Set("action-nonce", lNonce)
|
||||
if auth.Session.Save() != nil {
|
||||
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var userWithName *database.User
|
||||
var hasTwoFactor bool
|
||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||
userWithName, err = tx.GetUserDisplayName(auth.Data.ID)
|
||||
userWithName, err = tx.GetUserDisplayName(auth.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user display name: %w", err)
|
||||
}
|
||||
hasTwoFactor, err = tx.HasTwoFactor(auth.Data.ID)
|
||||
hasTwoFactor, err = tx.HasTwoFactor(auth.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user two factor state: %w", err)
|
||||
}
|
||||
userWithName.Role, err = tx.GetUserRole(auth.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user role: %w", err)
|
||||
}
|
||||
return
|
||||
}) {
|
||||
return
|
||||
@ -47,5 +54,6 @@ func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httproute
|
||||
"User": userWithName,
|
||||
"Nonce": lNonce,
|
||||
"OtpEnabled": hasTwoFactor,
|
||||
"IsAdmin": userWithName.Role,
|
||||
})
|
||||
}
|
||||
|
@ -1,16 +1,15 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/1f349/mjwt/auth"
|
||||
"github.com/1f349/mjwt/claims"
|
||||
"github.com/1f349/tulip/database"
|
||||
"github.com/1f349/tulip/pages"
|
||||
"github.com/emersion/go-message/mail"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/google/uuid"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@ -100,12 +99,12 @@ func (h *HttpServer) LoginPost(rw http.ResponseWriter, req *http.Request, _ http
|
||||
return
|
||||
}
|
||||
|
||||
u := uuid.New()
|
||||
u := uuid.NewString()
|
||||
h.mailLinkCache.Set(mailLinkKey{mailLinkVerifyEmail, u}, userInfo.Sub, time.Now().Add(10*time.Minute))
|
||||
|
||||
// try to send email
|
||||
err = h.conf.Mail.SendEmailTemplate("mail-verify", "Verify Email", userInfo.Name, address, map[string]any{
|
||||
"VerifyUrl": h.conf.BaseUrl + "/mail/verify/" + u.String(),
|
||||
"VerifyUrl": h.conf.BaseUrl + "/mail/verify/" + u,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("[Tulip] Login: Failed to send verification email:", err)
|
||||
@ -122,14 +121,10 @@ func (h *HttpServer) LoginPost(rw http.ResponseWriter, req *http.Request, _ http
|
||||
}
|
||||
|
||||
// only continues if the above tx succeeds
|
||||
auth.Data = SessionData{
|
||||
auth = UserAuth{
|
||||
ID: userInfo.Sub,
|
||||
NeedOtp: hasOtp,
|
||||
}
|
||||
if auth.SaveSessionData() != nil {
|
||||
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if hasOtp {
|
||||
originUrl, err := url.Parse(req.FormValue("redirect"))
|
||||
@ -142,31 +137,36 @@ func (h *HttpServer) LoginPost(rw http.ResponseWriter, req *http.Request, _ http
|
||||
return
|
||||
}
|
||||
|
||||
if h.setLoginDataCookie(rw, auth.Data.ID) {
|
||||
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
|
||||
if h.setLoginDataCookie(rw, auth) {
|
||||
return
|
||||
}
|
||||
h.SafeRedirect(rw, req)
|
||||
}
|
||||
|
||||
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId uuid.UUID) bool {
|
||||
encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, h.signingKey.PublicKey(), userId[:], []byte("tulip-login-data"))
|
||||
var oneYear = 365 * 24 * time.Hour
|
||||
|
||||
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, authData UserAuth) bool {
|
||||
ps := claims.NewPermStorage()
|
||||
if authData.NeedOtp {
|
||||
ps.Set("needs-otp")
|
||||
}
|
||||
gen, err := h.signingKey.GenerateJwt(authData.ID, uuid.NewString(), jwt.ClaimStrings{h.conf.BaseUrl}, oneYear, auth.AccessTokenClaims{Perms: ps})
|
||||
if err != nil {
|
||||
http.Error(rw, "Failed to generate cookie token", http.StatusInternalServerError)
|
||||
return true
|
||||
}
|
||||
encryptedString := base64.RawStdEncoding.EncodeToString(encryptedData)
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "tulip-login-data",
|
||||
Value: encryptedString,
|
||||
Value: gen,
|
||||
Path: "/",
|
||||
Expires: time.Now().AddDate(0, 3, 0),
|
||||
Expires: time.Now().AddDate(1, 0, 0),
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *HttpServer) LoginResetPasswordPost(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
func (h *HttpServer) LoginResetPasswordPost(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
|
||||
email := req.PostFormValue("email")
|
||||
address, err := mail.ParseAddress(email)
|
||||
if err != nil || address.Name != "" {
|
||||
|
@ -4,21 +4,14 @@ import (
|
||||
"github.com/1f349/tulip/database"
|
||||
"github.com/1f349/tulip/pages"
|
||||
"github.com/emersion/go-message/mail"
|
||||
"github.com/go-session/session"
|
||||
"github.com/google/uuid"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (h *HttpServer) MailVerify(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
func (h *HttpServer) MailVerify(rw http.ResponseWriter, _ *http.Request, params httprouter.Params) {
|
||||
code := params.ByName("code")
|
||||
parse, err := uuid.Parse(code)
|
||||
if err != nil {
|
||||
http.Error(rw, "Invalid email verification code", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
k := mailLinkKey{mailLinkVerifyEmail, parse}
|
||||
k := mailLinkKey{mailLinkVerifyEmail, code}
|
||||
|
||||
userSub, ok := h.mailLinkCache.Get(k)
|
||||
if !ok {
|
||||
@ -36,45 +29,26 @@ func (h *HttpServer) MailVerify(rw http.ResponseWriter, req *http.Request, param
|
||||
http.Error(rw, "Email address has been verified, you may close this tab and return to the login page.", http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *HttpServer) MailPassword(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
func (h *HttpServer) MailPassword(rw http.ResponseWriter, _ *http.Request, params httprouter.Params) {
|
||||
code := params.ByName("code")
|
||||
parse, err := uuid.Parse(code)
|
||||
if err != nil {
|
||||
http.Error(rw, "Invalid password reset code", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
k := mailLinkKey{mailLinkResetPassword, parse}
|
||||
|
||||
userSub, ok := h.mailLinkCache.Get(k)
|
||||
k := mailLinkKey{mailLinkResetPassword, code}
|
||||
_, ok := h.mailLinkCache.Get(k)
|
||||
if !ok {
|
||||
http.Error(rw, "Invalid password reset code", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h.mailLinkCache.Delete(k)
|
||||
|
||||
ss, err := session.Start(req.Context(), rw, req)
|
||||
if err != nil {
|
||||
http.Error(rw, "Error loading session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ss.Set("mail-reset-password-user", userSub)
|
||||
err = ss.Save()
|
||||
if err != nil {
|
||||
http.Error(rw, "Error saving session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
pages.RenderPageTemplate(rw, "reset-password", map[string]any{
|
||||
"ServiceName": h.conf.ServiceName,
|
||||
"Code": code,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *HttpServer) MailPasswordPost(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
func (h *HttpServer) MailPasswordPost(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
|
||||
pw := req.PostFormValue("new_password")
|
||||
rpw := req.PostFormValue("confirm_password")
|
||||
code := req.PostFormValue("code")
|
||||
|
||||
// reverse passwords are possible
|
||||
if len(pw) == 0 {
|
||||
@ -91,25 +65,15 @@ func (h *HttpServer) MailPasswordPost(rw http.ResponseWriter, req *http.Request,
|
||||
return
|
||||
}
|
||||
|
||||
// start session
|
||||
ss, err := session.Start(req.Context(), rw, req)
|
||||
if err != nil {
|
||||
http.Error(rw, "Error loading session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// get user to reset password for from session
|
||||
userRaw, found := ss.Get("mail-reset-password-user")
|
||||
if !found {
|
||||
http.Error(rw, "Invalid password reset code", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
userSub, ok := userRaw.(uuid.UUID)
|
||||
k := mailLinkKey{mailLinkResetPassword, code}
|
||||
userSub, ok := h.mailLinkCache.Get(k)
|
||||
if !ok {
|
||||
http.Error(rw, "Invalid password reset code", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h.mailLinkCache.Delete(k)
|
||||
|
||||
// reset password database call
|
||||
if h.DbTx(rw, func(tx *database.Tx) error {
|
||||
return tx.UserResetPassword(userSub, pw)
|
||||
@ -120,16 +84,10 @@ func (h *HttpServer) MailPasswordPost(rw http.ResponseWriter, req *http.Request,
|
||||
http.Error(rw, "Reset password successfully, you can login now.", http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *HttpServer) MailDelete(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
func (h *HttpServer) MailDelete(rw http.ResponseWriter, _ *http.Request, params httprouter.Params) {
|
||||
code := params.ByName("code")
|
||||
parse, err := uuid.Parse(code)
|
||||
if err != nil {
|
||||
http.Error(rw, "Invalid email delete code", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
k := mailLinkKey{mailLinkDelete, parse}
|
||||
|
||||
k := mailLinkKey{mailLinkDelete, code}
|
||||
userSub, ok := h.mailLinkCache.Get(k)
|
||||
if !ok {
|
||||
http.Error(rw, "Invalid email delete code", http.StatusBadRequest)
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"github.com/1f349/tulip/database"
|
||||
"github.com/1f349/tulip/pages"
|
||||
"github.com/go-oauth2/oauth2/v4"
|
||||
"github.com/google/uuid"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -26,11 +25,11 @@ func (h *HttpServer) ManageAppsGet(rw http.ResponseWriter, req *http.Request, _
|
||||
var role database.UserRole
|
||||
var appList []database.ClientInfoDbOutput
|
||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||
role, err = tx.GetUserRole(auth.Data.ID)
|
||||
role, err = tx.GetUserRole(auth.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
appList, err = tx.GetAppList(auth.Data.ID, role == database.RoleAdmin, offset)
|
||||
appList, err = tx.GetAppList(auth.ID, role == database.RoleAdmin, offset)
|
||||
return
|
||||
}) {
|
||||
return
|
||||
@ -79,7 +78,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
|
||||
if sso {
|
||||
var role database.UserRole
|
||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||
role, err = tx.GetUserRole(auth.Data.ID)
|
||||
role, err = tx.GetUserRole(auth.ID)
|
||||
return
|
||||
}) {
|
||||
return
|
||||
@ -93,17 +92,13 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
|
||||
switch action {
|
||||
case "create":
|
||||
if h.DbTx(rw, func(tx *database.Tx) error {
|
||||
return tx.InsertClientApp(name, domain, public, sso, active, auth.Data.ID)
|
||||
return tx.InsertClientApp(name, domain, public, sso, active, auth.ID)
|
||||
}) {
|
||||
return
|
||||
}
|
||||
case "edit":
|
||||
if h.DbTx(rw, func(tx *database.Tx) error {
|
||||
sub, err := uuid.Parse(req.Form.Get("subject"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.UpdateClientApp(sub, auth.Data.ID, name, domain, public, sso, active)
|
||||
return tx.UpdateClientApp(req.Form.Get("subject"), auth.ID, name, domain, public, sso, active)
|
||||
}) {
|
||||
return
|
||||
}
|
||||
@ -111,15 +106,12 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
|
||||
var info oauth2.ClientInfo
|
||||
var secret string
|
||||
if h.DbTx(rw, func(tx *database.Tx) error {
|
||||
sub, err := uuid.Parse(req.Form.Get("subject"))
|
||||
sub := req.Form.Get("subject")
|
||||
info, err = tx.GetClientInfo(sub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err = tx.GetClientInfo(sub.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secret, err = tx.ResetClientAppSecret(sub, auth.Data.ID)
|
||||
secret, err = tx.ResetClientAppSecret(sub, auth.ID)
|
||||
return err
|
||||
}) {
|
||||
return
|
||||
|
@ -30,7 +30,7 @@ func (h *HttpServer) ManageUsersGet(rw http.ResponseWriter, req *http.Request, _
|
||||
var role database.UserRole
|
||||
var userList []database.User
|
||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||
role, err = tx.GetUserRole(auth.Data.ID)
|
||||
role, err = tx.GetUserRole(auth.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -49,12 +49,12 @@ func (h *HttpServer) ManageUsersGet(rw http.ResponseWriter, req *http.Request, _
|
||||
"Users": userList,
|
||||
"Offset": offset,
|
||||
"EmailShow": req.URL.Query().Has("show-email"),
|
||||
"CurrentAdmin": auth.Data.ID,
|
||||
"CurrentAdmin": auth.ID,
|
||||
"Namespace": h.conf.Namespace,
|
||||
}
|
||||
if q.Has("edit") {
|
||||
for _, i := range userList {
|
||||
if i.Sub.String() == q.Get("edit") {
|
||||
if i.Sub == q.Get("edit") {
|
||||
m["Edit"] = i
|
||||
goto validEdit
|
||||
}
|
||||
@ -78,7 +78,7 @@ func (h *HttpServer) ManageUsersPost(rw http.ResponseWriter, req *http.Request,
|
||||
|
||||
var role database.UserRole
|
||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||
role, err = tx.GetUserRole(auth.Data.ID)
|
||||
role, err = tx.GetUserRole(auth.ID)
|
||||
return
|
||||
}) {
|
||||
return
|
||||
@ -123,12 +123,12 @@ func (h *HttpServer) ManageUsersPost(rw http.ResponseWriter, req *http.Request,
|
||||
return
|
||||
}
|
||||
|
||||
u, u2 := uuid.New(), uuid.New()
|
||||
h.mailLinkCache.Set(mailLinkKey{mailLinkResetPassword, u}, userSub, time.Now().Add(10*time.Minute))
|
||||
h.mailLinkCache.Set(mailLinkKey{mailLinkDelete, u2}, userSub, time.Now().Add(10*time.Minute))
|
||||
u, u2 := uuid.NewString(), uuid.NewString()
|
||||
h.mailLinkCache.Set(mailLinkKey{mailLinkResetPassword, u}, userSub.String(), time.Now().Add(10*time.Minute))
|
||||
h.mailLinkCache.Set(mailLinkKey{mailLinkDelete, u2}, userSub.String(), time.Now().Add(10*time.Minute))
|
||||
|
||||
err = h.conf.Mail.SendEmailTemplate("mail-register-admin", "Register", name, address, map[string]any{
|
||||
"RegisterUrl": h.conf.BaseUrl + "/mail/password/" + u.String(),
|
||||
"RegisterUrl": h.conf.BaseUrl + "/mail/password/" + u,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("[Tulip] Login: Failed to send register email:", err)
|
||||
@ -137,10 +137,7 @@ func (h *HttpServer) ManageUsersPost(rw http.ResponseWriter, req *http.Request,
|
||||
}
|
||||
case "edit":
|
||||
if h.DbTx(rw, func(tx *database.Tx) error {
|
||||
sub, err := uuid.Parse(req.Form.Get("subject"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sub := req.Form.Get("subject")
|
||||
return tx.UpdateUser(sub, newRole, active)
|
||||
}) {
|
||||
return
|
||||
|
@ -80,11 +80,11 @@ func (h *HttpServer) authorizeEndpoint(rw http.ResponseWriter, req *http.Request
|
||||
var user *database.User
|
||||
var hasOtp bool
|
||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||
user, err = tx.GetUserDisplayName(auth.Data.ID)
|
||||
user, err = tx.GetUserDisplayName(auth.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hasOtp, err = tx.HasTwoFactor(auth.Data.ID)
|
||||
hasOtp, err = tx.HasTwoFactor(auth.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -120,7 +120,7 @@ func (h *HttpServer) authorizeEndpoint(rw http.ResponseWriter, req *http.Request
|
||||
|
||||
if !isSSO {
|
||||
otpInput := req.FormValue("code")
|
||||
if h.fetchAndValidateOtp(rw, auth.Data.ID, otpInput) {
|
||||
if h.fetchAndValidateOtp(rw, auth.ID, otpInput) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -150,7 +150,7 @@ func (h *HttpServer) oauthUserAuthorization(rw http.ResponseWriter, req *http.Re
|
||||
return "", err
|
||||
}
|
||||
|
||||
auth, err := internalAuthenticationHandler(rw, req)
|
||||
auth, err := h.internalAuthenticationHandler(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -172,5 +172,5 @@ func (h *HttpServer) oauthUserAuthorization(rw http.ResponseWriter, req *http.Re
|
||||
http.Redirect(rw, req, redirectUrl.String(), http.StatusFound)
|
||||
return "", nil
|
||||
}
|
||||
return auth.Data.ID.String(), nil
|
||||
return auth.ID, nil
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"encoding/base64"
|
||||
"github.com/1f349/tulip/database"
|
||||
"github.com/1f349/tulip/pages"
|
||||
"github.com/google/uuid"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"github.com/xlzd/gotp"
|
||||
@ -16,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func (h *HttpServer) LoginOtpGet(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {
|
||||
if !auth.Data.NeedOtp {
|
||||
if !auth.NeedOtp {
|
||||
h.SafeRedirect(rw, req)
|
||||
return
|
||||
}
|
||||
@ -28,27 +27,23 @@ func (h *HttpServer) LoginOtpGet(rw http.ResponseWriter, req *http.Request, _ ht
|
||||
}
|
||||
|
||||
func (h *HttpServer) LoginOtpPost(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {
|
||||
if !auth.Data.NeedOtp {
|
||||
if !auth.NeedOtp {
|
||||
http.Redirect(rw, req, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
otpInput := req.FormValue("code")
|
||||
if h.fetchAndValidateOtp(rw, auth.Data.ID, otpInput) {
|
||||
if h.fetchAndValidateOtp(rw, auth.ID, otpInput) {
|
||||
return
|
||||
}
|
||||
|
||||
auth.Data.NeedOtp = false
|
||||
if auth.SaveSessionData() != nil {
|
||||
http.Error(rw, "500 Internal Server Error: Failed to safe session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
auth.NeedOtp = false
|
||||
|
||||
h.setLoginDataCookie(rw, auth.Data.ID)
|
||||
h.setLoginDataCookie(rw, auth)
|
||||
h.SafeRedirect(rw, req)
|
||||
}
|
||||
|
||||
func (h *HttpServer) fetchAndValidateOtp(rw http.ResponseWriter, sub uuid.UUID, code string) bool {
|
||||
func (h *HttpServer) fetchAndValidateOtp(rw http.ResponseWriter, sub, code string) bool {
|
||||
var hasOtp bool
|
||||
var secret string
|
||||
var digits int
|
||||
@ -87,12 +82,12 @@ func (h *HttpServer) EditOtpPost(rw http.ResponseWriter, req *http.Request, _ ht
|
||||
}
|
||||
|
||||
otpInput := req.Form.Get("code")
|
||||
if h.fetchAndValidateOtp(rw, auth.Data.ID, otpInput) {
|
||||
if h.fetchAndValidateOtp(rw, auth.ID, otpInput) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.DbTx(rw, func(tx *database.Tx) error {
|
||||
return tx.SetTwoFactor(auth.Data.ID, "", 0)
|
||||
return tx.SetTwoFactor(auth.ID, "", 0)
|
||||
}) {
|
||||
return
|
||||
}
|
||||
@ -125,7 +120,7 @@ func (h *HttpServer) EditOtpPost(rw http.ResponseWriter, req *http.Request, _ ht
|
||||
var email string
|
||||
if h.DbTx(rw, func(tx *database.Tx) error {
|
||||
var err error
|
||||
email, err = tx.GetUserEmail(auth.Data.ID)
|
||||
email, err = tx.GetUserEmail(auth.ID)
|
||||
return err
|
||||
}) {
|
||||
return
|
||||
@ -173,7 +168,7 @@ func (h *HttpServer) EditOtpPost(rw http.ResponseWriter, req *http.Request, _ ht
|
||||
}
|
||||
|
||||
if h.DbTx(rw, func(tx *database.Tx) error {
|
||||
return tx.SetTwoFactor(auth.Data.ID, secret, digits)
|
||||
return tx.SetTwoFactor(auth.ID, secret, digits)
|
||||
}) {
|
||||
return
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ import (
|
||||
"github.com/go-oauth2/oauth2/v4/server"
|
||||
"github.com/go-oauth2/oauth2/v4/store"
|
||||
"github.com/go-session/session"
|
||||
"github.com/google/uuid"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"log"
|
||||
"net/http"
|
||||
@ -39,7 +38,7 @@ type HttpServer struct {
|
||||
signingKey mjwt.Signer
|
||||
|
||||
// mailLinkCache contains a mapping of verify uuids to user uuids
|
||||
mailLinkCache *cache.Cache[mailLinkKey, uuid.UUID]
|
||||
mailLinkCache *cache.Cache[mailLinkKey, string]
|
||||
}
|
||||
|
||||
const (
|
||||
@ -50,7 +49,7 @@ const (
|
||||
|
||||
type mailLinkKey struct {
|
||||
action byte
|
||||
data uuid.UUID
|
||||
data string
|
||||
}
|
||||
|
||||
func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Server {
|
||||
@ -82,7 +81,7 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
|
||||
conf: conf,
|
||||
signingKey: signingKey,
|
||||
|
||||
mailLinkCache: cache.New[mailLinkKey, uuid.UUID](),
|
||||
mailLinkCache: cache.New[mailLinkKey, string](),
|
||||
}
|
||||
|
||||
oauthManager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
|
||||
@ -124,18 +123,12 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
|
||||
})
|
||||
r.GET("/", hs.OptionalAuthentication(false, hs.Home))
|
||||
r.POST("/logout", hs.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
|
||||
lNonce, ok := auth.Session.Get("action-nonce")
|
||||
if !ok {
|
||||
http.Error(rw, "Missing nonce", http.StatusInternalServerError)
|
||||
cookie, err := req.Cookie("tulip-nonce")
|
||||
if err != nil {
|
||||
http.Error(rw, "Missing nonce", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(lNonce.(string)), []byte(req.PostFormValue("nonce"))) == 1 {
|
||||
auth.Session.Delete("session-data")
|
||||
if auth.Session.Save() != nil {
|
||||
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(cookie.Value), []byte(req.PostFormValue("nonce"))) == 1 {
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "tulip-login-data",
|
||||
Path: "/",
|
||||
@ -193,11 +186,6 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
|
||||
return
|
||||
}
|
||||
userId := token.GetUserID()
|
||||
userUuid, err := uuid.Parse(userId)
|
||||
if err != nil {
|
||||
http.Error(rw, "Invalid User ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Using token for user: %s by app: %s with scope: '%s'\n", userId, token.GetClientID(), token.GetScope())
|
||||
claims := ParseClaims(token.GetScope())
|
||||
@ -209,7 +197,7 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
|
||||
var userData *database.User
|
||||
|
||||
if hs.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||
userData, err = tx.GetUser(userUuid)
|
||||
userData, err = tx.GetUser(userId)
|
||||
return err
|
||||
}) {
|
||||
return
|
||||
|
Loading…
Reference in New Issue
Block a user