From b9d456f2faeb3856620ff9930664c74e2584208e Mon Sep 17 00:00:00 2001
From: MrMelon54
Date: Sat, 9 Sep 2023 01:38:10 +0100
Subject: [PATCH] Add OTP and fix up oauth with the new page rendering system
---
cmd/tulip/conf.go | 5 +-
cmd/tulip/serve.go | 7 +-
database/init.sql | 9 +
database/tx.go | 61 ++++-
go.mod | 4 +
go.sum | 20 +-
pages/edit-otp.go.html | 24 ++
pages/edit-password.go | 1 +
pages/edit-username.go | 1 +
pages/edit.go.html | 14 +-
pages/index.go.html | 14 ++
pages/login-otp.go.html | 21 ++
pages/login.go.html | 11 +-
...horize.go.html => oauth-authorize.go.html} | 7 +-
pages/pages.go | 8 +-
scope/scope.go | 51 ++++
scope/scope_test.go | 26 ++
server/auth.go | 67 ++++--
server/edit.go | 71 ++++++
server/home.go | 42 ++++
server/login.go | 72 ++++++
server/oauth.go | 116 +++++----
server/otp.go | 159 +++++++++++++
server/server.go | 222 ++++--------------
24 files changed, 735 insertions(+), 298 deletions(-)
create mode 100644 pages/edit-otp.go.html
create mode 100644 pages/edit-password.go
create mode 100644 pages/edit-username.go
create mode 100644 pages/login-otp.go.html
rename pages/{authorize.go.html => oauth-authorize.go.html} (71%)
create mode 100644 scope/scope.go
create mode 100644 scope/scope_test.go
create mode 100644 server/edit.go
create mode 100644 server/home.go
create mode 100644 server/login.go
create mode 100644 server/otp.go
diff --git a/cmd/tulip/conf.go b/cmd/tulip/conf.go
index e2e7572..9ab5243 100644
--- a/cmd/tulip/conf.go
+++ b/cmd/tulip/conf.go
@@ -1,6 +1,7 @@
package main
type startUpConfig struct {
- Listen string `json:"listen"`
- Domain string `json:"domain"`
+ Listen string `json:"listen"`
+ Domain string `json:"domain"`
+ OtpIssuer string `json:"otp_issuer"`
}
diff --git a/cmd/tulip/serve.go b/cmd/tulip/serve.go
index 213ea45..bebdf4f 100644
--- a/cmd/tulip/serve.go
+++ b/cmd/tulip/serve.go
@@ -8,7 +8,6 @@ import (
"errors"
"flag"
"fmt"
- clientStore "github.com/1f349/tulip/client-store"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/server"
"github.com/1f349/violet/utils"
@@ -36,7 +35,7 @@ func (s *serveCmd) Usage() string {
`
}
-func (s *serveCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
+func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...any) subcommands.ExitStatus {
log.Println("[Tulip] Starting...")
if s.configPath == "" {
@@ -83,9 +82,7 @@ func normalLoad(startUp startUpConfig, wd string) {
log.Fatal("[Tulip] Failed check:", err)
}
- cs := clientStore.New(db)
-
- srv := server.NewHttpServer(startUp.Listen, startUp.Domain, db, key, cs)
+ srv := server.NewHttpServer(startUp.Listen, startUp.Domain, startUp.OtpIssuer, db, key)
log.Printf("[Tulip] Starting HTTP server on '%s'\n", srv.Addr)
go utils.RunBackgroundHttp("HTTP", srv)
diff --git a/database/init.sql b/database/init.sql
index 40bdbd1..9d9cdf5 100644
--- a/database/init.sql
+++ b/database/init.sql
@@ -16,6 +16,8 @@ CREATE TABLE IF NOT EXISTS users
active INTEGER DEFAULT 1
);
+CREATE UNIQUE INDEX IF NOT EXISTS username_index ON users (username);
+
CREATE TABLE IF NOT EXISTS client_store
(
subject TEXT PRIMARY KEY UNIQUE NOT NULL,
@@ -25,3 +27,10 @@ CREATE TABLE IF NOT EXISTS client_store
sso INTEGER,
active INTEGER DEFAULT 1
);
+
+CREATE TABLE IF NOT EXISTS otp
+(
+ subject TEXT PRIMARY KEY UNIQUE NOT NULL,
+ raw BLOB NOT NULL,
+ FOREIGN KEY (subject) REFERENCES users (subject)
+);
diff --git a/database/tx.go b/database/tx.go
index fb42ae1..d8082ec 100644
--- a/database/tx.go
+++ b/database/tx.go
@@ -4,6 +4,7 @@ import (
"database/sql"
"fmt"
"github.com/1f349/tulip/password"
+ "github.com/1f349/twofactor"
"github.com/go-oauth2/oauth2/v4"
"github.com/google/uuid"
"time"
@@ -45,15 +46,16 @@ func (t *Tx) InsertUser(name, un, pw, email string) error {
return err
}
-func (t *Tx) CheckLogin(un, pw string) (*User, error) {
+func (t *Tx) CheckLogin(un, pw string) (*User, bool, error) {
var u User
- row := t.tx.QueryRow(`SELECT subject, password FROM users WHERE username = ? LIMIT 1`, un)
- err := row.Scan(&u.Sub, &u.Password)
+ var hasOtp bool
+ row := t.tx.QueryRow(`SELECT subject, password, EXISTS(SELECT 1 FROM otp WHERE otp.subject = users.subject) FROM users WHERE username = ?`, un)
+ err := row.Scan(&u.Sub, &u.Password, &hasOtp)
if err != nil {
- return nil, err
+ return nil, false, err
}
err = password.CheckPasswordHash(u.Password, pw)
- return &u, err
+ return &u, hasOtp, err
}
func (t *Tx) GetUserDisplayName(sub uuid.UUID) (*User, error) {
@@ -66,12 +68,19 @@ func (t *Tx) GetUserDisplayName(sub uuid.UUID) (*User, error) {
func (t *Tx) GetUser(sub uuid.UUID) (*User, error) {
var u User
- row := t.tx.QueryRow(`SELECT name, username, password, picture, website, email, email_verified, pronouns, birthdate, zoneinfo, locale, updated_at, active FROM users WHERE subject = ? LIMIT 1`, sub.String())
+ row := t.tx.QueryRow(`SELECT name, username, password, picture, website, email, email_verified, pronouns, birthdate, zoneinfo, locale, updated_at, active FROM users WHERE subject = ?`, sub.String())
err := row.Scan(&u.Name, &u.Username, &u.Password, &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) {
+ var email string
+ row := t.tx.QueryRow(`SELECT email FROM users WHERE subject = ?`, sub.String())
+ err := row.Scan(&email)
+ return email, err
+}
+
func (t *Tx) ChangeUserPassword(sub uuid.UUID, pwOld, pwNew string) error {
q, err := t.tx.Query(`SELECT password FROM users WHERE subject = ?`, sub)
if err != nil {
@@ -149,17 +158,40 @@ WHERE subject = ?`,
return nil
}
+func (t *Tx) SetTwoFactor(sub uuid.UUID, totp *twofactor.Totp) error {
+ u, err := totp.ToBytes()
+ if err != nil {
+ return err
+ }
+ _, err = t.tx.Exec(`INSERT INTO otp(subject, raw) VALUES (?, ?) ON CONFLICT(subject) DO UPDATE SET raw = excluded.raw`, sub.String(), u)
+ return err
+}
+
+func (t *Tx) GetTwoFactor(sub uuid.UUID, issuer string) (*twofactor.Totp, error) {
+ var u []byte
+ row := t.tx.QueryRow(`SELECT raw FROM otp WHERE subject = ?`, sub.String())
+ err := row.Scan(&u)
+ if err != nil {
+ return nil, err
+ }
+ return twofactor.TOTPFromBytes(u, issuer)
+}
+
func (t *Tx) GetClientInfo(sub string) (oauth2.ClientInfo, error) {
var u clientInfoDbOutput
- row := t.tx.QueryRow(`SELECT secret, domain, sso, active FROM client_store WHERE subject = ? LIMIT 1`, sub)
- err := row.Scan(&u.secret, &u.domain, &u.sso)
+ var active bool
+ row := t.tx.QueryRow(`SELECT secret, name, domain, sso, active FROM client_store WHERE subject = ? LIMIT 1`, sub)
+ err := row.Scan(&u.secret, &u.name, &u.domain, &u.sso, &active)
u.sub = sub
+ if !active {
+ return nil, fmt.Errorf("client is not active")
+ }
return &u, err
}
type clientInfoDbOutput struct {
- sub, secret, domain string
- sso bool
+ sub, name, secret, domain string
+ sso bool
}
func (c *clientInfoDbOutput) GetID() string { return c.sub }
@@ -167,4 +199,11 @@ func (c *clientInfoDbOutput) GetSecret() string { return c.secret }
func (c *clientInfoDbOutput) GetDomain() string { return c.domain }
func (c *clientInfoDbOutput) IsPublic() bool { return false }
func (c *clientInfoDbOutput) GetUserID() string { return "" }
-func (c *clientInfoDbOutput) IsSSO() bool { return c.sso }
+
+// IsSSO is an extra field for the oauth handler to skip the user input stage
+// this is for trusted applications to get permissions without asking the user
+func (c *clientInfoDbOutput) IsSSO() bool { return c.sso }
+
+// GetName is an extra field for the oauth handler to display the application
+// name
+func (c *clientInfoDbOutput) GetName() string { return c.name }
diff --git a/go.mod b/go.mod
index 7a44844..f407054 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/1f349/tulip
go 1.20
require (
+ github.com/1f349/twofactor v1.0.4
github.com/1f349/violet v0.0.9
github.com/MrMelon54/exit-reload v0.0.1
github.com/MrMelon54/pronouns v1.0.1
@@ -21,6 +22,9 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/sec51/convert v1.0.2 // indirect
+ github.com/sec51/gf256 v0.0.0-20160126143050-2454accbeb9e // indirect
+ github.com/sec51/qrcode v0.0.0-20160126144534-b7779abbcaf1 // indirect
github.com/tidwall/btree v1.6.0 // indirect
github.com/tidwall/buntdb v1.3.0 // indirect
github.com/tidwall/gjson v1.16.0 // indirect
diff --git a/go.sum b/go.sum
index 79f2c46..bd6c4a6 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-github.com/1f349/violet v0.0.7 h1:FxCAIVjzUzkgGfhGMX7FcvGj+kaJky45PnLfqKNgA8M=
-github.com/1f349/violet v0.0.7/go.mod h1:YfKZX9p55Uot8iSDnbqQbAgU717H0rFNo8ieu2wbxI4=
+github.com/1f349/twofactor v1.0.4 h1:kN4EEGFlKRa7fGrxS+FpgwJI+tllES6YzXqCqurk4Uk=
+github.com/1f349/twofactor v1.0.4/go.mod h1:gnG80vElwqLWNMnLT57yu4o4L1GdXGPP6pcIPlapXZs=
github.com/1f349/violet v0.0.9 h1:eQfc5fDMKJXVFUjS2UiAGTkOVVBamppD5dguhmU4GeU=
github.com/1f349/violet v0.0.9/go.mod h1:Uzu6I1pLBP5UEzcUCTQBbk/NTfI5TAABSrowa8DSpR0=
github.com/MrMelon54/exit-reload v0.0.1 h1:sxHa59tNEQMcikwuX2+93lw6Vi1+R7oCRF8a0C3alXc=
@@ -44,14 +44,12 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
@@ -82,6 +80,12 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
+github.com/sec51/convert v1.0.2 h1:NoKWIRARjM3rQglNypMpcXSLLqPsN/uTTzaGeqDKbeg=
+github.com/sec51/convert v1.0.2/go.mod h1:5qL/cT/oiOIvWXy2SccQ7LnacYftqqy9wdyFkTc1k2w=
+github.com/sec51/gf256 v0.0.0-20160126143050-2454accbeb9e h1:wKXba8dfsFjbxkMpzZBKt8gkJAMSm1fIf1OSWQFQrVA=
+github.com/sec51/gf256 v0.0.0-20160126143050-2454accbeb9e/go.mod h1:hCjOqSOB9PBw5MdJ+0uSLCBV7FbLy0xwOR+c193HkcE=
+github.com/sec51/qrcode v0.0.0-20160126144534-b7779abbcaf1 h1:CI9zS8HvMiibvXM/F3IthY797GW77fNYgioJl/8Xzzk=
+github.com/sec51/qrcode v0.0.0-20160126144534-b7779abbcaf1/go.mod h1:uPm44Rj3uXSSOvmKmoeRuAUNUgwH2JHW5KIzqFFS/j4=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@@ -103,8 +107,6 @@ github.com/tidwall/buntdb v1.3.0 h1:gdhWO+/YwoB2qZMeAU9JcWWsHSYU3OvcieYgFRS0zwA=
github.com/tidwall/buntdb v1.3.0/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/gjson v1.15.0 h1:5n/pM+v3r5ujuNl4YLZLsQ+UE5jlkLVm7jMzT5Mpolw=
-github.com/tidwall/gjson v1.15.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg=
github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
@@ -144,8 +146,6 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
-golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -180,8 +180,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
-golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/pages/edit-otp.go.html b/pages/edit-otp.go.html
new file mode 100644
index 0000000..c80719a
--- /dev/null
+++ b/pages/edit-otp.go.html
@@ -0,0 +1,24 @@
+
+
+
+ 1f349 ID
+
+
+
+
+
+
+
+
diff --git a/pages/edit-password.go b/pages/edit-password.go
new file mode 100644
index 0000000..76e382d
--- /dev/null
+++ b/pages/edit-password.go
@@ -0,0 +1 @@
+package pages
diff --git a/pages/edit-username.go b/pages/edit-username.go
new file mode 100644
index 0000000..76e382d
--- /dev/null
+++ b/pages/edit-username.go
@@ -0,0 +1 @@
+package pages
diff --git a/pages/edit.go.html b/pages/edit.go.html
index 31bace3..e328599 100644
--- a/pages/edit.go.html
+++ b/pages/edit.go.html
@@ -13,19 +13,19 @@
")
+ _, _ = fmt.Fprintln(rw, "400 Bad Request: Failed to parse form data, press the back button in your browser, check your inputs and try again.
")
+ _, _ = fmt.Fprintln(rw, "")
+ for _, i := range errs {
+ _, _ = fmt.Fprintf(rw, " - %s
\n", i)
+ }
+ _, _ = fmt.Fprintln(rw, "
")
+ _, _ = fmt.Fprintln(rw, "\n