Replace session with single login cookie, just use strings for user ids

This commit is contained in:
Melon 2024-02-09 15:24:40 +00:00
parent e822172513
commit 552ec72ded
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
17 changed files with 175 additions and 335 deletions

View File

@ -5,14 +5,13 @@ import (
"fmt" "fmt"
"github.com/MrMelon54/pronouns" "github.com/MrMelon54/pronouns"
"github.com/go-oauth2/oauth2/v4" "github.com/go-oauth2/oauth2/v4"
"github.com/google/uuid"
"golang.org/x/text/language" "golang.org/x/text/language"
"net/url" "net/url"
"time" "time"
) )
type User struct { type User struct {
Sub uuid.UUID `json:"sub"` Sub string `json:"sub"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Username string `json:"username"` Username string `json:"username"`
Picture NullStringScanner `json:"picture,omitempty"` Picture NullStringScanner `json:"picture,omitempty"`

View File

@ -59,37 +59,37 @@ func (t *Tx) CheckLogin(un, pw string) (*User, bool, bool, error) {
return &u, hasOtp, hasVerify, err 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 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) err := row.Scan(&u.Name)
u.Sub = sub u.Sub = sub
return &u, err return &u, err
} }
func (t *Tx) GetUserRole(sub uuid.UUID) (UserRole, error) { func (t *Tx) GetUserRole(sub string) (UserRole, error) {
var r UserRole 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) err := row.Scan(&r)
return r, err return r, err
} }
func (t *Tx) GetUser(sub uuid.UUID) (*User, error) { func (t *Tx) GetUser(sub string) (*User, error) {
var u User 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) 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 u.Sub = sub
return &u, err return &u, err
} }
func (t *Tx) GetUserEmail(sub uuid.UUID) (string, error) { func (t *Tx) GetUserEmail(sub string) (string, error) {
var email string 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) err := row.Scan(&email)
return email, err 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) q, err := t.tx.Query(`SELECT password FROM users WHERE subject = ?`, sub)
if err != nil { if err != nil {
return err return err
@ -131,7 +131,7 @@ func (t *Tx) ChangeUserPassword(sub uuid.UUID, pwOld, pwNew string) error {
return nil 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( exec, err := t.tx.Exec(
`UPDATE users `UPDATE users
SET name = ?, SET name = ?,
@ -166,19 +166,19 @@ WHERE subject = ?`,
return nil 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 { 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 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 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 secret string
var digits int 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) err := row.Scan(&secret, &digits)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
@ -186,7 +186,7 @@ func (t *Tx) GetTwoFactor(sub uuid.UUID) (string, int, error) {
return secret, digits, nil return secret, digits, nil
} }
func (t *Tx) HasTwoFactor(sub uuid.UUID) (bool, error) { func (t *Tx) HasTwoFactor(sub string) (bool, error) {
var hasOtp bool var hasOtp bool
row := t.tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM otp WHERE otp.subject = ?)`, sub) row := t.tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM otp WHERE otp.subject = ?)`, sub)
err := row.Scan(&hasOtp) err := row.Scan(&hasOtp)
@ -207,9 +207,9 @@ func (t *Tx) GetClientInfo(sub string) (oauth2.ClientInfo, error) {
return &u, err 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 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 { if err != nil {
return nil, err return nil, err
} }
@ -225,27 +225,27 @@ func (t *Tx) GetAppList(owner uuid.UUID, admin bool, offset int) ([]ClientInfoDb
return u, row.Err() 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() u := uuid.New()
secret, err := password.GenerateApiSecret(70) secret, err := password.GenerateApiSecret(70)
if err != nil { if err != nil {
return err 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 return err
} }
func (t *Tx) UpdateClientApp(subject, owner uuid.UUID, name, domain string, public, sso, active bool) error { 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.String(), owner.String()) _, 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 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) secret, err := password.GenerateApiSecret(70)
if err != nil { if err != nil {
return "", err 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 return secret, err
} }
@ -266,22 +266,22 @@ func (t *Tx) GetUserList(offset int) ([]User, error) {
return u, row.Err() 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) _, err := t.tx.Exec(`UPDATE users SET active = ?, role = ? WHERE subject = ?`, active, role, subject)
return err return err
} }
func (t *Tx) VerifyUserEmail(sub uuid.UUID) error { func (t *Tx) VerifyUserEmail(sub string) error {
_, err := t.tx.Exec(`UPDATE users SET email_verified = 1 WHERE subject = ?`, sub.String()) _, err := t.tx.Exec(`UPDATE users SET email_verified = 1 WHERE subject = ?`, sub)
return err 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) hashPassword, err := password.HashPassword(pw)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }

View File

@ -20,7 +20,7 @@ func TestTx_ChangeUserPassword(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
tx, err := d.Begin() tx, err := d.Begin()
assert.NoError(t, err) assert.NoError(t, err)
err = tx.ChangeUserPassword(u, "test", "new") err = tx.ChangeUserPassword(u.String(), "test", "new")
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, tx.Commit()) assert.NoError(t, tx.Commit())
query, err := d.db.Query(`SELECT password FROM users WHERE subject = ? AND username = ?`, u.String(), "test") 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) assert.NoError(t, err)
tx, err := d.Begin() tx, err := d.Begin()
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, tx.ModifyUser(u, &UserPatch{ assert.NoError(t, tx.ModifyUser(u.String(), &UserPatch{
Name: "example", Name: "example",
Pronouns: pronouns.TheyThem, Pronouns: pronouns.TheyThem,
ZoneInfo: time.UTC, ZoneInfo: time.UTC,

9
go.mod
View File

@ -4,7 +4,7 @@ go 1.22
require ( require (
github.com/1f349/cache v0.0.2 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/overlapfs v0.0.1
github.com/1f349/violet v0.0.13 github.com/1f349/violet v0.0.13
github.com/MrMelon54/exit-reload v0.0.1 github.com/MrMelon54/exit-reload v0.0.1
@ -14,6 +14,7 @@ require (
github.com/emersion/go-smtp v0.20.2 github.com/emersion/go-smtp v0.20.2
github.com/go-oauth2/oauth2/v4 v4.5.2 github.com/go-oauth2/oauth2/v4 v4.5.2
github.com/go-session/session v3.1.2+incompatible 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/subcommands v1.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/julienschmidt/httprouter v1.3.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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
github.com/xlzd/gotp v0.1.0 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 golang.org/x/text v0.14.0
) )
require ( require (
github.com/MrMelon54/rescheduler v0.0.2 // indirect 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/davecgh/go-spew v1.1.1 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // 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 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/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/tidwall/btree v1.7.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/pretty v1.2.1 // indirect
github.com/tidwall/rtred v0.1.2 // indirect github.com/tidwall/rtred v0.1.2 // indirect
github.com/tidwall/tinyqueue v0.1.1 // 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 gopkg.in/yaml.v3 v3.0.1 // indirect
) )

14
go.sum
View File

@ -1,8 +1,8 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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 h1:27QD6zPd9xYyvh9V1qqWq+EAt5+N+qvyGWKfnjMrhP8=
github.com/1f349/cache v0.0.2/go.mod h1:LibAMy13dF0KO1fQA9aEjZPBCB6Y4b5kKYEQJUqc2rQ= 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.2 h1:mVw71zcf0D7dWgZXMvjXMq8oNn41V1DFyLY0Ywkq1VQ=
github.com/1f349/mjwt v0.2.1/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU= 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 h1:LAxBolrXFAgU0yqZtXg/C/aaPq3eoQSPpBc49BHuTp0=
github.com/1f349/overlapfs v0.0.1/go.mod h1:I6aItQycr7nrzplmfNXp/QF9tTmKRSgY3fXmu/7Ky2o= github.com/1f349/overlapfs v0.0.1/go.mod h1:I6aItQycr7nrzplmfNXp/QF9tTmKRSgY3fXmu/7Ky2o=
github.com/1f349/violet v0.0.13 h1:lJpTz15Ea83Uc1VAISXTjtKuzr8Pe8NM4cMGp3Aiyhk= 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/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 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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-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-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.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= 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.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/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-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.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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= 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/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-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -10,21 +10,14 @@
</header> </header>
<main> <main>
<form method="POST" action="/mail/password"> <form method="POST" action="/mail/password">
<input type="hidden" name="code" value="{{.Code}}"/>
<div> <div>
<label for="field_new_password">New Password:</label> <label for="field_new_password">New Password:</label>
<input type="password" <input type="password" name="new_password" id="field_new_password" autocomplete="new_password" required/>
name="new_password"
id="field_new_password"
autocomplete="new_password"
required/>
</div> </div>
<div> <div>
<label for="field_confirm_password">Confirm Password:</label> <label for="field_confirm_password">Confirm Password:</label>
<input type="password" <input type="password" name="confirm_password" id="field_confirm_password" autocomplete="confirm_password" required/>
name="confirm_password"
id="field_confirm_password"
autocomplete="confirm_password"
required/>
</div> </div>
<button type="submit">Login</button> <button type="submit">Login</button>
</form> </form>

View File

@ -1,14 +1,9 @@
package server package server
import ( import (
"crypto/rand" "github.com/1f349/mjwt"
"crypto/rsa" "github.com/1f349/mjwt/auth"
"crypto/sha256"
"encoding/base64"
"fmt"
"github.com/1f349/tulip/database" "github.com/1f349/tulip/database"
"github.com/go-session/session"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"net/http" "net/http"
"net/url" "net/url"
@ -18,36 +13,26 @@ import (
type UserHandler func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) type UserHandler func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth)
type UserAuth struct { type UserAuth struct {
Session session.Store ID string
Data SessionData
}
type SessionData struct {
ID uuid.UUID
NeedOtp bool NeedOtp bool
} }
func (u UserAuth) NextFlowUrl(origin *url.URL) *url.URL { func (u UserAuth) NextFlowUrl(origin *url.URL) *url.URL {
if u.Data.NeedOtp { if u.NeedOtp {
return PrepareRedirectUrl("/login/otp", origin) return PrepareRedirectUrl("/login/otp", origin)
} }
return nil return nil
} }
func (u UserAuth) IsGuest() bool { func (u UserAuth) IsGuest() bool {
return u.Data.ID == uuid.Nil return u.ID == ""
}
func (u UserAuth) SaveSessionData() error {
u.Session.Set("session-data", u.Data)
return u.Session.Save()
} }
func (h *HttpServer) RequireAdminAuthentication(next UserHandler) httprouter.Handle { func (h *HttpServer) RequireAdminAuthentication(next UserHandler) httprouter.Handle {
return h.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) { return h.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
var role database.UserRole var role database.UserRole
if h.DbTx(rw, func(tx *database.Tx) (err error) { 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
}) { }) {
return return
@ -73,54 +58,27 @@ func (h *HttpServer) RequireAuthentication(next UserHandler) httprouter.Handle {
func (h *HttpServer) OptionalAuthentication(flowPart bool, 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) { return func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
auth, err := internalAuthenticationHandler(rw, req) authData, err := h.internalAuthenticationHandler(req)
if err != nil { if err == nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) if n := authData.NextFlowUrl(req.URL); n != nil && !flowPart {
return
}
if n := auth.NextFlowUrl(req.URL); n != nil && !flowPart {
http.Redirect(rw, req, n.String(), http.StatusFound) http.Redirect(rw, req, n.String(), http.StatusFound)
return return
} }
if auth.IsGuest() { }
next(rw, req, params, authData)
}
}
func (h *HttpServer) internalAuthenticationHandler(req *http.Request) (UserAuth, error) {
if loginCookie, err := req.Cookie("tulip-login-data"); err == nil { if loginCookie, err := req.Cookie("tulip-login-data"); err == nil {
if decryptedBytes, err := base64.RawStdEncoding.DecodeString(loginCookie.Value); err == nil { _, b, err := mjwt.ExtractClaims[auth.AccessTokenClaims](h.signingKey, loginCookie.Value)
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
}
}
}
}
}
next(rw, req, params, auth)
}
}
func internalAuthenticationHandler(rw http.ResponseWriter, req *http.Request) (UserAuth, error) {
ss, err := session.Start(req.Context(), rw, req)
if err != nil { if err != nil {
return UserAuth{}, fmt.Errorf("failed to start session") return UserAuth{}, err
} }
return UserAuth{ID: b.Subject, NeedOtp: b.Claims.Perms.Has("need-otp")}, nil
// get auth object
userIdRaw, ok := ss.Get("session-data")
if !ok {
return UserAuth{Session: ss}, nil
} }
userData, ok := userIdRaw.(SessionData) // not logged in
if !ok { return UserAuth{}, nil
ss.Delete("session-data")
err := ss.Save()
if err != nil {
return UserAuth{Session: ss}, fmt.Errorf("failed to reset invalid session data")
}
}
return UserAuth{Session: ss, Data: userData}, nil
} }
func PrepareRedirectUrl(targetPath string, origin *url.URL) *url.URL { func PrepareRedirectUrl(targetPath string, origin *url.URL) *url.URL {

View File

@ -2,28 +2,26 @@ package server
import ( import (
"context" "context"
"fmt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"net/http" "net/http"
"net/http/httptest"
"net/url" "net/url"
"testing" "testing"
) )
func TestUserAuth_NextFlowUrl(t *testing.T) { 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"}, *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"}}.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()})) 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{})) assert.Nil(t, u.NextFlowUrl(&url.URL{}))
} }
func TestUserAuth_IsGuest(t *testing.T) { func TestUserAuth_IsGuest(t *testing.T) {
var u UserAuth var u UserAuth
assert.True(t, u.IsGuest()) assert.True(t, u.IsGuest())
u.Data.ID = uuid.New() u.ID = uuid.NewString()
assert.False(t, u.IsGuest()) assert.False(t, u.IsGuest())
} }
@ -42,70 +40,17 @@ func (f *fakeSessionStore) Get(key string) (a interface{}, ok bool) {
return 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 TestRequireAuthentication(t *testing.T) {
} }
func TestOptionalAuthentication(t *testing.T) { func TestOptionalAuthentication(t *testing.T) {
h := &HttpServer{}
req, err := http.NewRequest(http.MethodGet, "https://example.com/hello", nil) req, err := http.NewRequest(http.MethodGet, "https://example.com/hello", nil)
assert.NoError(t, err) assert.NoError(t, err)
rec := httptest.NewRecorder() auth, err := h.internalAuthenticationHandler(req)
auth, err := internalAuthenticationHandler(rec, req)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, auth.IsGuest()) assert.True(t, auth.IsGuest())
auth.Data.ID = uuid.UUID{5, 6, 7} auth.ID = "567"
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)
} }
func TestPrepareRedirectUrl(t *testing.T) { func TestPrepareRedirectUrl(t *testing.T) {

View File

@ -8,6 +8,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"net/http" "net/http"
"time"
) )
func (h *HttpServer) EditGet(rw http.ResponseWriter, _ *http.Request, _ httprouter.Params, auth UserAuth) { 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 { if h.DbTx(rw, func(tx *database.Tx) error {
var err error var err error
user, err = tx.GetUser(auth.Data.ID) user, err = tx.GetUser(auth.ID)
if err != nil { if err != nil {
return fmt.Errorf("failed to read user data: %w", err) 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() lNonce := uuid.NewString()
auth.Session.Set("action-nonce", lNonce) http.SetCookie(rw, &http.Cookie{
if auth.Session.Save() != nil { Name: "tulip-nonce",
http.Error(rw, "Failed to save session", http.StatusInternalServerError) Value: lNonce,
return Path: "/",
} Expires: time.Now().Add(10 * time.Minute),
Secure: true,
SameSite: http.SameSiteStrictMode,
})
pages.RenderPageTemplate(rw, "edit", map[string]any{ pages.RenderPageTemplate(rw, "edit", map[string]any{
"ServiceName": h.conf.ServiceName, "ServiceName": h.conf.ServiceName,
"User": user, "User": user,
@ -61,7 +65,7 @@ func (h *HttpServer) EditPost(rw http.ResponseWriter, req *http.Request, _ httpr
return return
} }
if h.DbTx(rw, func(tx *database.Tx) error { 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 fmt.Errorf("failed to modify user info: %w", err)
} }
return nil return nil

View File

@ -7,11 +7,21 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"net/http" "net/http"
"time"
) )
func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) { func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {
rw.Header().Set("Content-Type", "text/html") 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() { if auth.IsGuest() {
pages.RenderPageTemplate(rw, "index-guest", map[string]any{ pages.RenderPageTemplate(rw, "index-guest", map[string]any{
"ServiceName": h.conf.ServiceName, "ServiceName": h.conf.ServiceName,
@ -19,24 +29,21 @@ func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httproute
return 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 userWithName *database.User
var hasTwoFactor bool var hasTwoFactor bool
if h.DbTx(rw, func(tx *database.Tx) (err error) { 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 { if err != nil {
return fmt.Errorf("failed to get user display name: %w", err) 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 { if err != nil {
return fmt.Errorf("failed to get user two factor state: %w", err) 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
}) { }) {
return return
@ -47,5 +54,6 @@ func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httproute
"User": userWithName, "User": userWithName,
"Nonce": lNonce, "Nonce": lNonce,
"OtpEnabled": hasTwoFactor, "OtpEnabled": hasTwoFactor,
"IsAdmin": userWithName.Role,
}) })
} }

View File

@ -1,16 +1,15 @@
package server package server
import ( import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"database/sql" "database/sql"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"github.com/1f349/mjwt/auth"
"github.com/1f349/mjwt/claims"
"github.com/1f349/tulip/database" "github.com/1f349/tulip/database"
"github.com/1f349/tulip/pages" "github.com/1f349/tulip/pages"
"github.com/emersion/go-message/mail" "github.com/emersion/go-message/mail"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -100,12 +99,12 @@ func (h *HttpServer) LoginPost(rw http.ResponseWriter, req *http.Request, _ http
return return
} }
u := uuid.New() u := uuid.NewString()
h.mailLinkCache.Set(mailLinkKey{mailLinkVerifyEmail, u}, userInfo.Sub, time.Now().Add(10*time.Minute)) h.mailLinkCache.Set(mailLinkKey{mailLinkVerifyEmail, u}, userInfo.Sub, time.Now().Add(10*time.Minute))
// try to send email // try to send email
err = h.conf.Mail.SendEmailTemplate("mail-verify", "Verify Email", userInfo.Name, address, map[string]any{ 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 { if err != nil {
log.Println("[Tulip] Login: Failed to send verification email:", err) 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 // only continues if the above tx succeeds
auth.Data = SessionData{ auth = UserAuth{
ID: userInfo.Sub, ID: userInfo.Sub,
NeedOtp: hasOtp, NeedOtp: hasOtp,
} }
if auth.SaveSessionData() != nil {
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
return
}
if hasOtp { if hasOtp {
originUrl, err := url.Parse(req.FormValue("redirect")) originUrl, err := url.Parse(req.FormValue("redirect"))
@ -142,31 +137,36 @@ func (h *HttpServer) LoginPost(rw http.ResponseWriter, req *http.Request, _ http
return return
} }
if h.setLoginDataCookie(rw, auth.Data.ID) { if h.setLoginDataCookie(rw, auth) {
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
return return
} }
h.SafeRedirect(rw, req) h.SafeRedirect(rw, req)
} }
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId uuid.UUID) bool { var oneYear = 365 * 24 * time.Hour
encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, h.signingKey.PublicKey(), userId[:], []byte("tulip-login-data"))
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 { if err != nil {
http.Error(rw, "Failed to generate cookie token", http.StatusInternalServerError)
return true return true
} }
encryptedString := base64.RawStdEncoding.EncodeToString(encryptedData)
http.SetCookie(rw, &http.Cookie{ http.SetCookie(rw, &http.Cookie{
Name: "tulip-login-data", Name: "tulip-login-data",
Value: encryptedString, Value: gen,
Path: "/", Path: "/",
Expires: time.Now().AddDate(0, 3, 0), Expires: time.Now().AddDate(1, 0, 0),
Secure: true, Secure: true,
SameSite: http.SameSiteStrictMode, SameSite: http.SameSiteStrictMode,
}) })
return false 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") email := req.PostFormValue("email")
address, err := mail.ParseAddress(email) address, err := mail.ParseAddress(email)
if err != nil || address.Name != "" { if err != nil || address.Name != "" {

View File

@ -4,21 +4,14 @@ import (
"github.com/1f349/tulip/database" "github.com/1f349/tulip/database"
"github.com/1f349/tulip/pages" "github.com/1f349/tulip/pages"
"github.com/emersion/go-message/mail" "github.com/emersion/go-message/mail"
"github.com/go-session/session"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"net/http" "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") 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) userSub, ok := h.mailLinkCache.Get(k)
if !ok { 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) 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") 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} k := mailLinkKey{mailLinkResetPassword, code}
_, ok := h.mailLinkCache.Get(k)
userSub, ok := h.mailLinkCache.Get(k)
if !ok { if !ok {
http.Error(rw, "Invalid password reset code", http.StatusBadRequest) http.Error(rw, "Invalid password reset code", http.StatusBadRequest)
return 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{ pages.RenderPageTemplate(rw, "reset-password", map[string]any{
"ServiceName": h.conf.ServiceName, "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") pw := req.PostFormValue("new_password")
rpw := req.PostFormValue("confirm_password") rpw := req.PostFormValue("confirm_password")
code := req.PostFormValue("code")
// reverse passwords are possible // reverse passwords are possible
if len(pw) == 0 { if len(pw) == 0 {
@ -91,25 +65,15 @@ func (h *HttpServer) MailPasswordPost(rw http.ResponseWriter, req *http.Request,
return return
} }
// start session k := mailLinkKey{mailLinkResetPassword, code}
ss, err := session.Start(req.Context(), rw, req) userSub, ok := h.mailLinkCache.Get(k)
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)
if !ok { if !ok {
http.Error(rw, "Invalid password reset code", http.StatusBadRequest) http.Error(rw, "Invalid password reset code", http.StatusBadRequest)
return return
} }
h.mailLinkCache.Delete(k)
// reset password database call // reset password database call
if h.DbTx(rw, func(tx *database.Tx) error { if h.DbTx(rw, func(tx *database.Tx) error {
return tx.UserResetPassword(userSub, pw) 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) 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") 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) userSub, ok := h.mailLinkCache.Get(k)
if !ok { if !ok {
http.Error(rw, "Invalid email delete code", http.StatusBadRequest) http.Error(rw, "Invalid email delete code", http.StatusBadRequest)

View File

@ -4,7 +4,6 @@ import (
"github.com/1f349/tulip/database" "github.com/1f349/tulip/database"
"github.com/1f349/tulip/pages" "github.com/1f349/tulip/pages"
"github.com/go-oauth2/oauth2/v4" "github.com/go-oauth2/oauth2/v4"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"net/http" "net/http"
"net/url" "net/url"
@ -26,11 +25,11 @@ func (h *HttpServer) ManageAppsGet(rw http.ResponseWriter, req *http.Request, _
var role database.UserRole var role database.UserRole
var appList []database.ClientInfoDbOutput var appList []database.ClientInfoDbOutput
if h.DbTx(rw, func(tx *database.Tx) (err error) { 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 { if err != nil {
return return
} }
appList, err = tx.GetAppList(auth.Data.ID, role == database.RoleAdmin, offset) appList, err = tx.GetAppList(auth.ID, role == database.RoleAdmin, offset)
return return
}) { }) {
return return
@ -79,7 +78,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
if sso { if sso {
var role database.UserRole var role database.UserRole
if h.DbTx(rw, func(tx *database.Tx) (err error) { 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
}) { }) {
return return
@ -93,17 +92,13 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
switch action { switch action {
case "create": case "create":
if h.DbTx(rw, func(tx *database.Tx) error { 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 return
} }
case "edit": case "edit":
if h.DbTx(rw, func(tx *database.Tx) error { if h.DbTx(rw, func(tx *database.Tx) error {
sub, err := uuid.Parse(req.Form.Get("subject")) return tx.UpdateClientApp(req.Form.Get("subject"), auth.ID, name, domain, public, sso, active)
if err != nil {
return err
}
return tx.UpdateClientApp(sub, auth.Data.ID, name, domain, public, sso, active)
}) { }) {
return return
} }
@ -111,15 +106,12 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
var info oauth2.ClientInfo var info oauth2.ClientInfo
var secret string var secret string
if h.DbTx(rw, func(tx *database.Tx) error { 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 { if err != nil {
return err return err
} }
info, err = tx.GetClientInfo(sub.String()) secret, err = tx.ResetClientAppSecret(sub, auth.ID)
if err != nil {
return err
}
secret, err = tx.ResetClientAppSecret(sub, auth.Data.ID)
return err return err
}) { }) {
return return

View File

@ -30,7 +30,7 @@ func (h *HttpServer) ManageUsersGet(rw http.ResponseWriter, req *http.Request, _
var role database.UserRole var role database.UserRole
var userList []database.User var userList []database.User
if h.DbTx(rw, func(tx *database.Tx) (err error) { 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 { if err != nil {
return return
} }
@ -49,12 +49,12 @@ func (h *HttpServer) ManageUsersGet(rw http.ResponseWriter, req *http.Request, _
"Users": userList, "Users": userList,
"Offset": offset, "Offset": offset,
"EmailShow": req.URL.Query().Has("show-email"), "EmailShow": req.URL.Query().Has("show-email"),
"CurrentAdmin": auth.Data.ID, "CurrentAdmin": auth.ID,
"Namespace": h.conf.Namespace, "Namespace": h.conf.Namespace,
} }
if q.Has("edit") { if q.Has("edit") {
for _, i := range userList { for _, i := range userList {
if i.Sub.String() == q.Get("edit") { if i.Sub == q.Get("edit") {
m["Edit"] = i m["Edit"] = i
goto validEdit goto validEdit
} }
@ -78,7 +78,7 @@ func (h *HttpServer) ManageUsersPost(rw http.ResponseWriter, req *http.Request,
var role database.UserRole var role database.UserRole
if h.DbTx(rw, func(tx *database.Tx) (err error) { 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
}) { }) {
return return
@ -123,12 +123,12 @@ func (h *HttpServer) ManageUsersPost(rw http.ResponseWriter, req *http.Request,
return return
} }
u, u2 := uuid.New(), uuid.New() u, u2 := uuid.NewString(), uuid.NewString()
h.mailLinkCache.Set(mailLinkKey{mailLinkResetPassword, u}, userSub, time.Now().Add(10*time.Minute)) h.mailLinkCache.Set(mailLinkKey{mailLinkResetPassword, u}, userSub.String(), time.Now().Add(10*time.Minute))
h.mailLinkCache.Set(mailLinkKey{mailLinkDelete, u2}, userSub, 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{ 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 { if err != nil {
log.Println("[Tulip] Login: Failed to send register email:", err) 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": case "edit":
if h.DbTx(rw, func(tx *database.Tx) error { if h.DbTx(rw, func(tx *database.Tx) error {
sub, err := uuid.Parse(req.Form.Get("subject")) sub := req.Form.Get("subject")
if err != nil {
return err
}
return tx.UpdateUser(sub, newRole, active) return tx.UpdateUser(sub, newRole, active)
}) { }) {
return return

View File

@ -80,11 +80,11 @@ func (h *HttpServer) authorizeEndpoint(rw http.ResponseWriter, req *http.Request
var user *database.User var user *database.User
var hasOtp bool var hasOtp bool
if h.DbTx(rw, func(tx *database.Tx) (err error) { 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 { if err != nil {
return return
} }
hasOtp, err = tx.HasTwoFactor(auth.Data.ID) hasOtp, err = tx.HasTwoFactor(auth.ID)
if err != nil { if err != nil {
return return
} }
@ -120,7 +120,7 @@ func (h *HttpServer) authorizeEndpoint(rw http.ResponseWriter, req *http.Request
if !isSSO { if !isSSO {
otpInput := req.FormValue("code") otpInput := req.FormValue("code")
if h.fetchAndValidateOtp(rw, auth.Data.ID, otpInput) { if h.fetchAndValidateOtp(rw, auth.ID, otpInput) {
return return
} }
} }
@ -150,7 +150,7 @@ func (h *HttpServer) oauthUserAuthorization(rw http.ResponseWriter, req *http.Re
return "", err return "", err
} }
auth, err := internalAuthenticationHandler(rw, req) auth, err := h.internalAuthenticationHandler(req)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -172,5 +172,5 @@ func (h *HttpServer) oauthUserAuthorization(rw http.ResponseWriter, req *http.Re
http.Redirect(rw, req, redirectUrl.String(), http.StatusFound) http.Redirect(rw, req, redirectUrl.String(), http.StatusFound)
return "", nil return "", nil
} }
return auth.Data.ID.String(), nil return auth.ID, nil
} }

View File

@ -5,7 +5,6 @@ import (
"encoding/base64" "encoding/base64"
"github.com/1f349/tulip/database" "github.com/1f349/tulip/database"
"github.com/1f349/tulip/pages" "github.com/1f349/tulip/pages"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/skip2/go-qrcode" "github.com/skip2/go-qrcode"
"github.com/xlzd/gotp" "github.com/xlzd/gotp"
@ -16,7 +15,7 @@ import (
) )
func (h *HttpServer) LoginOtpGet(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) { 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) h.SafeRedirect(rw, req)
return 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) { 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) http.Redirect(rw, req, "/", http.StatusFound)
return return
} }
otpInput := req.FormValue("code") otpInput := req.FormValue("code")
if h.fetchAndValidateOtp(rw, auth.Data.ID, otpInput) { if h.fetchAndValidateOtp(rw, auth.ID, otpInput) {
return return
} }
auth.Data.NeedOtp = false auth.NeedOtp = false
if auth.SaveSessionData() != nil {
http.Error(rw, "500 Internal Server Error: Failed to safe session", http.StatusInternalServerError)
return
}
h.setLoginDataCookie(rw, auth.Data.ID) h.setLoginDataCookie(rw, auth)
h.SafeRedirect(rw, req) 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 hasOtp bool
var secret string var secret string
var digits int var digits int
@ -87,12 +82,12 @@ func (h *HttpServer) EditOtpPost(rw http.ResponseWriter, req *http.Request, _ ht
} }
otpInput := req.Form.Get("code") otpInput := req.Form.Get("code")
if h.fetchAndValidateOtp(rw, auth.Data.ID, otpInput) { if h.fetchAndValidateOtp(rw, auth.ID, otpInput) {
return return
} }
if h.DbTx(rw, func(tx *database.Tx) error { if h.DbTx(rw, func(tx *database.Tx) error {
return tx.SetTwoFactor(auth.Data.ID, "", 0) return tx.SetTwoFactor(auth.ID, "", 0)
}) { }) {
return return
} }
@ -125,7 +120,7 @@ func (h *HttpServer) EditOtpPost(rw http.ResponseWriter, req *http.Request, _ ht
var email string var email string
if h.DbTx(rw, func(tx *database.Tx) error { if h.DbTx(rw, func(tx *database.Tx) error {
var err error var err error
email, err = tx.GetUserEmail(auth.Data.ID) email, err = tx.GetUserEmail(auth.ID)
return err return err
}) { }) {
return 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 { 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 return
} }

View File

@ -19,7 +19,6 @@ import (
"github.com/go-oauth2/oauth2/v4/server" "github.com/go-oauth2/oauth2/v4/server"
"github.com/go-oauth2/oauth2/v4/store" "github.com/go-oauth2/oauth2/v4/store"
"github.com/go-session/session" "github.com/go-session/session"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"log" "log"
"net/http" "net/http"
@ -39,7 +38,7 @@ type HttpServer struct {
signingKey mjwt.Signer signingKey mjwt.Signer
// mailLinkCache contains a mapping of verify uuids to user uuids // mailLinkCache contains a mapping of verify uuids to user uuids
mailLinkCache *cache.Cache[mailLinkKey, uuid.UUID] mailLinkCache *cache.Cache[mailLinkKey, string]
} }
const ( const (
@ -50,7 +49,7 @@ const (
type mailLinkKey struct { type mailLinkKey struct {
action byte action byte
data uuid.UUID data string
} }
func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Server { 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, conf: conf,
signingKey: signingKey, signingKey: signingKey,
mailLinkCache: cache.New[mailLinkKey, uuid.UUID](), mailLinkCache: cache.New[mailLinkKey, string](),
} }
oauthManager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) 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.GET("/", hs.OptionalAuthentication(false, hs.Home))
r.POST("/logout", hs.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) { r.POST("/logout", hs.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
lNonce, ok := auth.Session.Get("action-nonce") cookie, err := req.Cookie("tulip-nonce")
if !ok { if err != nil {
http.Error(rw, "Missing nonce", http.StatusInternalServerError) http.Error(rw, "Missing nonce", http.StatusBadRequest)
return return
} }
if subtle.ConstantTimeCompare([]byte(lNonce.(string)), []byte(req.PostFormValue("nonce"))) == 1 { if subtle.ConstantTimeCompare([]byte(cookie.Value), []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
}
http.SetCookie(rw, &http.Cookie{ http.SetCookie(rw, &http.Cookie{
Name: "tulip-login-data", Name: "tulip-login-data",
Path: "/", Path: "/",
@ -193,11 +186,6 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
return return
} }
userId := token.GetUserID() 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()) fmt.Printf("Using token for user: %s by app: %s with scope: '%s'\n", userId, token.GetClientID(), token.GetScope())
claims := ParseClaims(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 var userData *database.User
if hs.DbTx(rw, func(tx *database.Tx) (err error) { if hs.DbTx(rw, func(tx *database.Tx) (err error) {
userData, err = tx.GetUser(userUuid) userData, err = tx.GetUser(userId)
return err return err
}) { }) {
return return