From b99fb9df6fc82b5a6979e63de4e54f56a28b33e3 Mon Sep 17 00:00:00 2001 From: MrMelon54 Date: Thu, 8 Feb 2024 01:16:14 +0000 Subject: [PATCH] Add public to oauth client store and separate fetching user info --- database/db-types.go | 4 +- database/init.sql | 1 + database/tx.go | 18 +++--- go.mod | 18 +++--- go.sum | 32 ++++++--- pages/index.go.html | 12 ++-- pages/manage-apps.go.html | 15 +++-- server/auth.go | 18 ++---- server/login.go | 132 ++++++++++++++++++++++++++++---------- server/manage-apps.go | 7 +- server/server.go | 6 +- server/userinfo.go | 21 ++++++ 12 files changed, 189 insertions(+), 95 deletions(-) create mode 100644 server/userinfo.go diff --git a/database/db-types.go b/database/db-types.go index cae329d..06c328c 100644 --- a/database/db-types.go +++ b/database/db-types.go @@ -16,7 +16,7 @@ type User struct { type ClientInfoDbOutput struct { Sub, Name, Secret, Domain, Owner string - SSO, Active bool + Public, SSO, Active bool } var _ oauth2.ClientInfo = &ClientInfoDbOutput{} @@ -24,7 +24,7 @@ var _ oauth2.ClientInfo = &ClientInfoDbOutput{} func (c *ClientInfoDbOutput) GetID() string { return c.Sub } 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) IsPublic() bool { return c.Public } func (c *ClientInfoDbOutput) GetUserID() string { return c.Owner } // GetName is an extra field for the oauth handler to display the application diff --git a/database/init.sql b/database/init.sql index 3ff0d6f..7271513 100644 --- a/database/init.sql +++ b/database/init.sql @@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS client_store secret TEXT UNIQUE NOT NULL, domain TEXT NOT NULL, owner TEXT NOT NULL, + public INTEGER, sso INTEGER, active INTEGER DEFAULT 1, FOREIGN KEY (owner) REFERENCES users (subject) diff --git a/database/tx.go b/database/tx.go index a7cc3a1..6b85021 100644 --- a/database/tx.go +++ b/database/tx.go @@ -65,8 +65,8 @@ func (t *Tx) GetUserEmail(sub string) (string, error) { func (t *Tx) GetClientInfo(sub string) (oauth2.ClientInfo, error) { var u ClientInfoDbOutput - 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, &u.Active) + row := t.tx.QueryRow(`SELECT secret, name, domain, public, sso, active FROM client_store WHERE subject = ? LIMIT 1`, sub) + err := row.Scan(&u.Secret, &u.Name, &u.Domain, &u.Public, &u.SSO, &u.Active) u.Owner = sub if !u.Active { return nil, fmt.Errorf("client is not active") @@ -74,16 +74,16 @@ func (t *Tx) GetClientInfo(sub string) (oauth2.ClientInfo, error) { return &u, err } -func (t *Tx) GetAppList(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, sso, active FROM client_store LIMIT 25 OFFSET ?`, 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 } defer row.Close() for row.Next() { var a ClientInfoDbOutput - err := row.Scan(&a.Sub, &a.Name, &a.Domain, &a.Owner, &a.SSO, &a.Active) + err := row.Scan(&a.Sub, &a.Name, &a.Domain, &a.Owner, &a.Public, &a.SSO, &a.Active) if err != nil { return nil, err } @@ -92,18 +92,18 @@ func (t *Tx) GetAppList(offset int) ([]ClientInfoDbOutput, error) { return u, row.Err() } -func (t *Tx) InsertClientApp(name, domain string, sso, active bool, owner string) 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, sso, active) VALUES (?, ?, ?, ?, ?, ?, ?)`, u.String(), name, secret, domain, owner, 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 uuid.UUID, owner string, name, domain string, sso, active bool) error { - _, err := t.tx.Exec(`UPDATE client_store SET name = ?, domain = ?, sso = ?, active = ? WHERE subject = ? AND owner = ?`, name, domain, sso, active, subject.String(), owner) +func (t *Tx) UpdateClientApp(subject uuid.UUID, owner string, name, domain string, public, sso, active bool) error { + _, err := t.tx.Exec(`UPDATE client_store SET name = ?, domain = ?, public = ?, sso = ?, active = ? WHERE subject = ? AND owner = ?`, name, domain, public, sso, active, subject.String(), owner) return err } diff --git a/go.mod b/go.mod index c446681..375cfd7 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/google/subcommands v1.2.0 github.com/google/uuid v1.6.0 github.com/julienschmidt/httprouter v1.3.0 - github.com/mattn/go-sqlite3 v1.14.18 + github.com/mattn/go-sqlite3 v1.14.22 github.com/stretchr/testify v1.8.4 golang.org/x/oauth2 v0.16.0 ) @@ -21,20 +21,20 @@ require ( require ( github.com/MrMelon54/rescheduler v0.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/golang-jwt/jwt v3.2.1+incompatible // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/kr/text v0.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect - github.com/tidwall/buntdb v1.1.2 // indirect - github.com/tidwall/gjson v1.12.1 // indirect - github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect + github.com/tidwall/btree v1.7.0 // indirect + github.com/tidwall/buntdb v1.3.0 // indirect + github.com/tidwall/gjson v1.17.0 // indirect + github.com/tidwall/grect v0.1.4 // indirect github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.0 // indirect - github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect - github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect + 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 google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.32.0 // indirect diff --git a/go.sum b/go.sum index 7fe8b45..a0da316 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,9 @@ github.com/go-oauth2/oauth2/v4 v4.5.2 h1:CuZhD3lhGuI6aNLyUbRHXsgG2RwGRBOuCBfd4WQ github.com/go-oauth2/oauth2/v4 v4.5.2/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ= github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg= github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= -github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -83,8 +84,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= -github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -112,25 +113,36 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E= +github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= +github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= -github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo= +github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= +github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI= +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 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M= +github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= +github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= +github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= +github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= +github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= -github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE= github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= +github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= +github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= diff --git a/pages/index.go.html b/pages/index.go.html index 1b04c0f..f831713 100644 --- a/pages/index.go.html +++ b/pages/index.go.html @@ -15,11 +15,13 @@ -
-
- -
-
+ {{if .IsAdmin}} +
+
+ +
+
+ {{end}}
diff --git a/pages/manage-apps.go.html b/pages/manage-apps.go.html index 3d05460..c508ceb 100644 --- a/pages/manage-apps.go.html +++ b/pages/manage-apps.go.html @@ -58,15 +58,16 @@
+
+ +
{{if .IsAdmin}}
- +
{{end}}
- +
@@ -131,14 +132,16 @@ +
+ +
{{if .IsAdmin}}
{{end}}
- +
diff --git a/server/auth.go b/server/auth.go index 385bb9a..d435c55 100644 --- a/server/auth.go +++ b/server/auth.go @@ -1,10 +1,6 @@ package server import ( - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "encoding/base64" "fmt" "github.com/1f349/lavender/database" "github.com/go-session/session" @@ -24,7 +20,7 @@ type UserAuth struct { type SessionData struct { ID string DisplayName string - UserInfo map[string]any + UserInfo UserInfoFields } func (u UserAuth) IsGuest() bool { @@ -45,7 +41,7 @@ func (h *HttpServer) RequireAdminAuthentication(next UserHandler) httprouter.Han }) { return } - if HasRole(roles, "lavender:admin") { + if !HasRole(roles, "lavender:admin") { http.Error(rw, "403 Forbidden", http.StatusForbidden) return } @@ -71,14 +67,8 @@ func (h *HttpServer) OptionalAuthentication(next UserHandler) httprouter.Handle http.Error(rw, err.Error(), http.StatusInternalServerError) return } - if auth.IsGuest() { - if loginCookie, err := req.Cookie("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("login-data")); err == nil { - auth.Data.ID = string(decryptedData) - } - } - } + if auth.IsGuest() && !h.readLoginDataCookie(rw, req, &auth) { + return } next(rw, req, params, auth) } diff --git a/server/login.go b/server/login.go index e3e7ca8..77b371f 100644 --- a/server/login.go +++ b/server/login.go @@ -1,12 +1,17 @@ package server import ( + "bytes" "context" "crypto/rand" "crypto/rsa" "crypto/sha256" + "database/sql" "encoding/base64" "encoding/json" + "errors" + "github.com/1f349/lavender/database" + "github.com/1f349/lavender/issuer" "github.com/1f349/lavender/pages" "github.com/google/uuid" "github.com/julienschmidt/httprouter" @@ -106,61 +111,52 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _ return } - res, err := flowState.sso.OAuth2Config.Client(context.Background(), token).Get(flowState.sso.UserInfoEndpoint) - if err != nil || res.StatusCode != 200 { - rw.WriteHeader(http.StatusInternalServerError) - if err != nil { - _, _ = rw.Write([]byte(err.Error())) - } else { - _, _ = rw.Write([]byte(res.Status)) + sessionData, done := h.fetchUserInfo(rw, err, flowState.sso, token) + if !done { + return + } + + if h.DbTx(rw, func(tx *database.Tx) error { + _, err := tx.GetUser(sessionData.ID) + if errors.Is(err, sql.ErrNoRows) { + uEmail := sessionData.UserInfo.GetStringOrDefault("email", "unknown@localhost") + uEmailVerified, _ := sessionData.UserInfo.GetBoolean("email_verified") + return tx.InsertUser(sessionData.ID, uEmail, uEmailVerified, "", true) } + return err + }) { return } - defer res.Body.Close() - - var userInfoJson map[string]any - if err := json.NewDecoder(res.Body).Decode(&userInfoJson); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - subject, ok := userInfoJson["sub"].(string) - if !ok { - http.Error(rw, "Invalid subject", http.StatusInternalServerError) - return - } - subject += "@" + flowState.sso.Config.Namespace - - displayName, ok := userInfoJson["name"].(string) - if !ok { - displayName = "Unknown Name" - } // only continues if the above tx succeeds - auth.Data = SessionData{ - ID: subject, - DisplayName: displayName, - UserInfo: userInfoJson, - } + auth.Data = sessionData if auth.SaveSessionData() != nil { http.Error(rw, "Failed to save session", http.StatusInternalServerError) return } - if h.setLoginDataCookie(rw, auth.Data.ID) { + if h.setLoginDataCookie(rw, auth.Data.ID, token) { http.Error(rw, "Internal Server Error", http.StatusInternalServerError) return } h.SafeRedirect(rw, req) } -func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId string) bool { - encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, h.signingKey.PublicKey(), []byte(userId), []byte("login-data")) +func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId string, token *oauth2.Token) bool { + buf := new(bytes.Buffer) + buf.WriteString(userId) + buf.WriteByte(0) + err := json.NewEncoder(buf).Encode(token) + if err != nil { + return true + } + encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, h.signingKey.PublicKey(), buf.Bytes(), []byte("lavender-login-data")) if err != nil { return true } encryptedString := base64.RawStdEncoding.EncodeToString(encryptedData) http.SetCookie(rw, &http.Cookie{ - Name: "login-data", + Name: "lavender-login-data", Value: encryptedString, Path: "/", Expires: time.Now().AddDate(0, 3, 0), @@ -169,3 +165,71 @@ func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId string) b }) return false } + +func (h *HttpServer) readLoginDataCookie(rw http.ResponseWriter, req *http.Request, u *UserAuth) bool { + loginCookie, err := req.Cookie("lavender-login-data") + if err != nil { + return false + } + decryptedBytes, err := base64.RawStdEncoding.DecodeString(loginCookie.Value) + if err != nil { + return false + } + decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, h.signingKey.PrivateKey(), decryptedBytes, []byte("lavender-login-data")) + if err != nil { + return false + } + + buf := bytes.NewBuffer(decryptedData) + userId, err := buf.ReadString(0) + if err != nil { + return false + } + userId = strings.TrimSuffix(userId, "\x00") + + var token *oauth2.Token + err = json.NewDecoder(buf).Decode(&token) + if err != nil { + return false + } + + sso := h.manager.FindServiceFromLogin(userId) + if sso == nil { + return false + } + + sessionData, done := h.fetchUserInfo(rw, err, sso, token) + if !done { + return false + } + + u.Data = sessionData + return true +} + +func (h *HttpServer) fetchUserInfo(rw http.ResponseWriter, err error, sso *issuer.WellKnownOIDC, token *oauth2.Token) (SessionData, bool) { + res, err := sso.OAuth2Config.Client(context.Background(), token).Get(sso.UserInfoEndpoint) + if err != nil || res.StatusCode != http.StatusOK { + return SessionData{}, false + } + defer res.Body.Close() + + var userInfoJson UserInfoFields + if err := json.NewDecoder(res.Body).Decode(&userInfoJson); err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return SessionData{}, false + } + subject, ok := userInfoJson.GetString("sub") + if !ok { + http.Error(rw, "Invalid subject", http.StatusInternalServerError) + return SessionData{}, false + } + subject += "@" + sso.Config.Namespace + + displayName := userInfoJson.GetStringOrDefault("name", "Unknown Name") + return SessionData{ + ID: subject, + DisplayName: displayName, + UserInfo: userInfoJson, + }, true +} diff --git a/server/manage-apps.go b/server/manage-apps.go index 5d5b6a8..4083a26 100644 --- a/server/manage-apps.go +++ b/server/manage-apps.go @@ -30,7 +30,7 @@ func (h *HttpServer) ManageAppsGet(rw http.ResponseWriter, req *http.Request, _ if err != nil { return } - appList, err = tx.GetAppList(offset) + appList, err = tx.GetAppList(auth.Data.ID, HasRole(roles, "lavender:admin"), offset) return }) { return @@ -72,6 +72,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _ action := req.Form.Get("action") name := req.Form.Get("name") domain := req.Form.Get("domain") + public := req.Form.Has("public") sso := req.Form.Has("sso") active := req.Form.Has("active") @@ -92,7 +93,7 @@ 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, sso, active, auth.Data.ID) + return tx.InsertClientApp(name, domain, public, sso, active, auth.Data.ID) }) { return } @@ -102,7 +103,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _ if err != nil { return err } - return tx.UpdateClientApp(sub, auth.Data.ID, name, domain, sso, active) + return tx.UpdateClientApp(sub, auth.Data.ID, name, domain, public, sso, active) }) { return } diff --git a/server/server.go b/server/server.go index b3f9ed3..7d466db 100644 --- a/server/server.go +++ b/server/server.go @@ -137,7 +137,7 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser } http.SetCookie(rw, &http.Cookie{ - Name: "login-data", + Name: "lavender-login-data", Path: "/", MaxAge: -1, Secure: true, @@ -156,8 +156,8 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser }) // management pages - r.GET("/manage/apps", hs.RequireAdminAuthentication(hs.ManageAppsGet)) - r.POST("/manage/apps", hs.RequireAdminAuthentication(hs.ManageAppsPost)) + r.GET("/manage/apps", hs.RequireAuthentication(hs.ManageAppsGet)) + r.POST("/manage/apps", hs.RequireAuthentication(hs.ManageAppsPost)) r.GET("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersGet)) r.POST("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersPost)) diff --git a/server/userinfo.go b/server/userinfo.go new file mode 100644 index 0000000..e40ed49 --- /dev/null +++ b/server/userinfo.go @@ -0,0 +1,21 @@ +package server + +type UserInfoFields map[string]any + +func (u UserInfoFields) GetString(key string) (string, bool) { + s, ok := u[key].(string) + return s, ok +} + +func (u UserInfoFields) GetStringOrDefault(key, other string) string { + s, ok := u.GetString(key) + if !ok { + s = other + } + return s +} + +func (u UserInfoFields) GetBoolean(key string) (bool, bool) { + b, ok := u[key].(bool) + return b, ok +}