diff --git a/database/db-types.go b/database/db-types.go index 2686efb..d368445 100644 --- a/database/db-types.go +++ b/database/db-types.go @@ -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"` diff --git a/database/tx.go b/database/tx.go index db3ab75..fec526e 100644 --- a/database/tx.go +++ b/database/tx.go @@ -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 } diff --git a/database/tx_test.go b/database/tx_test.go index 09fef36..7e90224 100644 --- a/database/tx_test.go +++ b/database/tx_test.go @@ -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, diff --git a/go.mod b/go.mod index 663cdec..b18fa4f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22 require ( github.com/1f349/cache v0.0.2 - github.com/1f349/mjwt v0.2.1 + github.com/1f349/mjwt v0.2.2 github.com/1f349/overlapfs v0.0.1 github.com/1f349/violet v0.0.13 github.com/MrMelon54/exit-reload v0.0.1 @@ -14,6 +14,7 @@ require ( github.com/emersion/go-smtp v0.20.2 github.com/go-oauth2/oauth2/v4 v4.5.2 github.com/go-session/session v3.1.2+incompatible + github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/subcommands v1.2.0 github.com/google/uuid v1.6.0 github.com/julienschmidt/httprouter v1.3.0 @@ -21,16 +22,16 @@ require ( github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/stretchr/testify v1.8.4 github.com/xlzd/gotp v0.1.0 - golang.org/x/crypto v0.18.0 + golang.org/x/crypto v0.19.0 golang.org/x/text v0.14.0 ) require ( github.com/MrMelon54/rescheduler v0.0.2 // indirect + github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tidwall/btree v1.7.0 // indirect @@ -41,6 +42,6 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/rtred v0.1.2 // indirect github.com/tidwall/tinyqueue v0.1.1 // indirect - golang.org/x/net v0.20.0 // indirect + golang.org/x/net v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6fb56e5..e6b7f33 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/1f349/cache v0.0.2 h1:27QD6zPd9xYyvh9V1qqWq+EAt5+N+qvyGWKfnjMrhP8= github.com/1f349/cache v0.0.2/go.mod h1:LibAMy13dF0KO1fQA9aEjZPBCB6Y4b5kKYEQJUqc2rQ= -github.com/1f349/mjwt v0.2.1 h1:REdiM/MaNjYQwHvI39LaMPhlvMg4Vy9SgomWMsKTNz8= -github.com/1f349/mjwt v0.2.1/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU= +github.com/1f349/mjwt v0.2.2 h1:mVw71zcf0D7dWgZXMvjXMq8oNn41V1DFyLY0Ywkq1VQ= +github.com/1f349/mjwt v0.2.2/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU= github.com/1f349/overlapfs v0.0.1 h1:LAxBolrXFAgU0yqZtXg/C/aaPq3eoQSPpBc49BHuTp0= github.com/1f349/overlapfs v0.0.1/go.mod h1:I6aItQycr7nrzplmfNXp/QF9tTmKRSgY3fXmu/7Ky2o= github.com/1f349/violet v0.0.13 h1:lJpTz15Ea83Uc1VAISXTjtKuzr8Pe8NM4cMGp3Aiyhk= @@ -17,6 +17,8 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= +github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -171,8 +173,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -187,8 +189,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/pages/reset-password.go.html b/pages/reset-password.go.html index 135c3a5..97dcebe 100644 --- a/pages/reset-password.go.html +++ b/pages/reset-password.go.html @@ -10,21 +10,14 @@
+
- +
- +
diff --git a/server/auth.go b/server/auth.go index 100b370..1d7d9a4 100644 --- a/server/auth.go +++ b/server/auth.go @@ -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 { diff --git a/server/auth_test.go b/server/auth_test.go index 5a56732..bb6356d 100644 --- a/server/auth_test.go +++ b/server/auth_test.go @@ -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) { diff --git a/server/edit.go b/server/edit.go index 8e35039..dfb05d0 100644 --- a/server/edit.go +++ b/server/edit.go @@ -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 diff --git a/server/home.go b/server/home.go index 362eeee..13dc16b 100644 --- a/server/home.go +++ b/server/home.go @@ -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, }) } diff --git a/server/login.go b/server/login.go index d783953..fe33b5c 100644 --- a/server/login.go +++ b/server/login.go @@ -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 != "" { diff --git a/server/mail.go b/server/mail.go index a26db0b..dbe591c 100644 --- a/server/mail.go +++ b/server/mail.go @@ -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) diff --git a/server/manage-apps.go b/server/manage-apps.go index 2cecb07..586174f 100644 --- a/server/manage-apps.go +++ b/server/manage-apps.go @@ -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 diff --git a/server/manage-users.go b/server/manage-users.go index 505d912..87d38f6 100644 --- a/server/manage-users.go +++ b/server/manage-users.go @@ -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 diff --git a/server/oauth.go b/server/oauth.go index fd971f2..907adf8 100644 --- a/server/oauth.go +++ b/server/oauth.go @@ -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 } diff --git a/server/otp.go b/server/otp.go index c2d6795..0e22b5b 100644 --- a/server/otp.go +++ b/server/otp.go @@ -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 } diff --git a/server/server.go b/server/server.go index 34ca07d..ae6bc5a 100644 --- a/server/server.go +++ b/server/server.go @@ -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