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"
"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"`

View File

@ -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
}

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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>

View File

@ -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 {

View File

@ -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) {

View File

@ -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

View File

@ -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,
})
}

View File

@ -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 != "" {

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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