Add support for public oauth client apps

This commit is contained in:
Melon 2024-02-08 01:16:46 +00:00
parent 33faf6aa5f
commit e822172513
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
9 changed files with 40 additions and 30 deletions

View File

@ -105,7 +105,7 @@ func (u *UserPatch) ParseFromForm(v url.Values) (safeErrs []error) {
type ClientInfoDbOutput struct { type ClientInfoDbOutput struct {
Sub, Name, Secret, Domain, Owner string Sub, Name, Secret, Domain, Owner string
SSO, Active bool Public, SSO, Active bool
} }
var _ oauth2.ClientInfo = &ClientInfoDbOutput{} var _ oauth2.ClientInfo = &ClientInfoDbOutput{}
@ -113,7 +113,7 @@ var _ oauth2.ClientInfo = &ClientInfoDbOutput{}
func (c *ClientInfoDbOutput) GetID() string { return c.Sub } func (c *ClientInfoDbOutput) GetID() string { return c.Sub }
func (c *ClientInfoDbOutput) GetSecret() string { return c.Secret } func (c *ClientInfoDbOutput) GetSecret() string { return c.Secret }
func (c *ClientInfoDbOutput) GetDomain() string { return c.Domain } 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 } func (c *ClientInfoDbOutput) GetUserID() string { return c.Owner }
// GetName is an extra field for the oauth handler to display the application // GetName is an extra field for the oauth handler to display the application

View File

@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS client_store
secret TEXT UNIQUE NOT NULL, secret TEXT UNIQUE NOT NULL,
domain TEXT NOT NULL, domain TEXT NOT NULL,
owner TEXT NOT NULL, owner TEXT NOT NULL,
public INTEGER,
sso INTEGER, sso INTEGER,
active INTEGER DEFAULT 1, active INTEGER DEFAULT 1,
FOREIGN KEY (owner) REFERENCES users (subject) FOREIGN KEY (owner) REFERENCES users (subject)

View File

@ -198,8 +198,8 @@ func (t *Tx) HasTwoFactor(sub uuid.UUID) (bool, error) {
func (t *Tx) GetClientInfo(sub string) (oauth2.ClientInfo, error) { func (t *Tx) GetClientInfo(sub string) (oauth2.ClientInfo, error) {
var u ClientInfoDbOutput var u ClientInfoDbOutput
row := t.tx.QueryRow(`SELECT secret, name, domain, sso, active FROM client_store WHERE subject = ? LIMIT 1`, sub) 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.SSO, &u.Active) err := row.Scan(&u.Secret, &u.Name, &u.Domain, &u.Public, &u.SSO, &u.Active)
u.Owner = sub u.Owner = sub
if !u.Active { if !u.Active {
return nil, fmt.Errorf("client is not active") return nil, fmt.Errorf("client is not active")
@ -207,16 +207,16 @@ func (t *Tx) GetClientInfo(sub string) (oauth2.ClientInfo, error) {
return &u, err return &u, err
} }
func (t *Tx) GetAppList(offset int) ([]ClientInfoDbOutput, error) { func (t *Tx) GetAppList(owner uuid.UUID, admin bool, offset int) ([]ClientInfoDbOutput, error) {
var u []ClientInfoDbOutput 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.String(), admin, offset)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer row.Close() defer row.Close()
for row.Next() { for row.Next() {
var a ClientInfoDbOutput 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 { if err != nil {
return nil, err return nil, err
} }
@ -225,18 +225,18 @@ func (t *Tx) GetAppList(offset int) ([]ClientInfoDbOutput, error) {
return u, row.Err() return u, row.Err()
} }
func (t *Tx) InsertClientApp(name, domain string, sso, active bool, owner uuid.UUID) error { func (t *Tx) InsertClientApp(name, domain string, public, sso, active bool, owner uuid.UUID) error {
u := uuid.New() u := uuid.New()
secret, err := password.GenerateApiSecret(70) secret, err := password.GenerateApiSecret(70)
if err != nil { if err != nil {
return err return err
} }
_, err = t.tx.Exec(`INSERT INTO client_store (subject, name, secret, domain, owner, sso, active) VALUES (?, ?, ?, ?, ?, ?, ?)`, u.String(), name, secret, domain, owner.String(), 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.String(), public, sso, active)
return err return err
} }
func (t *Tx) UpdateClientApp(subject, owner uuid.UUID, name, domain string, sso, active bool) error { 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 = ?, sso = ?, active = ? WHERE subject = ? AND owner = ?`, name, domain, sso, active, subject.String(), owner.String()) _, err := t.tx.Exec(`UPDATE client_store SET name = ?, domain = ?, public = ?, sso = ?, active = ? WHERE subject = ? AND owner = ?`, name, domain, public, sso, active, subject.String(), owner.String())
return err return err
} }

View File

@ -25,11 +25,13 @@
<button type="submit">Manage Applications</button> <button type="submit">Manage Applications</button>
</form> </form>
</div> </div>
{{if .IsAdmin}}
<div> <div>
<form method="GET" action="/manage/users"> <form method="GET" action="/manage/users">
<button type="submit">Manage Users</button> <button type="submit">Manage Users</button>
</form> </form>
</div> </div>
{{end}}
{{if .OtpEnabled}} {{if .OtpEnabled}}
<div> <div>
<form method="POST" action="/edit/otp"> <form method="POST" action="/edit/otp">

View File

@ -58,15 +58,16 @@
<label for="field_domain">Domain:</label> <label for="field_domain">Domain:</label>
<input type="text" name="domain" id="field_domain" value="{{.Edit.Domain}}" required/> <input type="text" name="domain" id="field_domain" value="{{.Edit.Domain}}" required/>
</div> </div>
<div>
<label for="field_public">Public: <input type="checkbox" name="public" id="field_public" {{if .Edit.Public}}checked{{end}}/></label>
</div>
{{if .IsAdmin}} {{if .IsAdmin}}
<div> <div>
<label for="field_sso">SSO: <input type="checkbox" name="sso" id="field_sso" <label for="field_sso">SSO: <input type="checkbox" name="sso" id="field_sso" {{if .Edit.SSO}}checked{{end}}/></label>
{{if .Edit.SSO}}checked{{end}}/></label>
</div> </div>
{{end}} {{end}}
<div> <div>
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active" <label for="field_active">Active: <input type="checkbox" name="active" id="field_active" {{if .Edit.Active}}checked{{end}}/></label>
{{if .Edit.Active}}checked{{end}}/></label>
</div> </div>
<button type="submit">Edit</button> <button type="submit">Edit</button>
</form> </form>
@ -85,6 +86,7 @@
<th>ID</th> <th>ID</th>
<th>Name</th> <th>Name</th>
<th>Domain</th> <th>Domain</th>
<th>Public</th>
<th>SSO</th> <th>SSO</th>
<th>Active</th> <th>Active</th>
<th>Owner</th> <th>Owner</th>
@ -97,6 +99,7 @@
<td>{{.Sub}}</td> <td>{{.Sub}}</td>
<td>{{.Name}}</td> <td>{{.Name}}</td>
<td>{{.Domain}}</td> <td>{{.Domain}}</td>
<td>{{.Public}}</td>
<td>{{.SSO}}</td> <td>{{.SSO}}</td>
<td>{{.Active}}</td> <td>{{.Active}}</td>
<td>{{.Owner}}</td> <td>{{.Owner}}</td>
@ -131,6 +134,9 @@
<label for="field_domain">Domain:</label> <label for="field_domain">Domain:</label>
<input type="text" name="domain" id="field_domain" required/> <input type="text" name="domain" id="field_domain" required/>
</div> </div>
<div>
<label for="field_public">Public: <input type="checkbox" name="public" id="field_public"/></label>
</div>
{{if .IsAdmin}} {{if .IsAdmin}}
<div> <div>
<label for="field_sso">SSO: <input type="checkbox" name="sso" id="field_sso"/></label> <label for="field_sso">SSO: <input type="checkbox" name="sso" id="field_sso"/></label>

View File

@ -83,9 +83,9 @@ func (h *HttpServer) OptionalAuthentication(flowPart bool, next UserHandler) htt
return return
} }
if auth.IsGuest() { if auth.IsGuest() {
if loginCookie, err := req.Cookie("login-data"); err == nil { if loginCookie, err := req.Cookie("tulip-login-data"); err == nil {
if decryptedBytes, err := base64.RawStdEncoding.DecodeString(loginCookie.Value); 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 { if decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, h.signingKey.PrivateKey(), decryptedBytes, []byte("tulip-login-data")); err == nil {
if len(decryptedData) == 16 { if len(decryptedData) == 16 {
var u uuid.UUID var u uuid.UUID
copy(u[:], decryptedData[:]) copy(u[:], decryptedData[:])

View File

@ -150,13 +150,13 @@ func (h *HttpServer) LoginPost(rw http.ResponseWriter, req *http.Request, _ http
} }
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId uuid.UUID) bool { func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId uuid.UUID) bool {
encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, h.signingKey.PublicKey(), userId[:], []byte("login-data")) encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, h.signingKey.PublicKey(), userId[:], []byte("tulip-login-data"))
if err != nil { if err != nil {
return true return true
} }
encryptedString := base64.RawStdEncoding.EncodeToString(encryptedData) encryptedString := base64.RawStdEncoding.EncodeToString(encryptedData)
http.SetCookie(rw, &http.Cookie{ http.SetCookie(rw, &http.Cookie{
Name: "login-data", Name: "tulip-login-data",
Value: encryptedString, Value: encryptedString,
Path: "/", Path: "/",
Expires: time.Now().AddDate(0, 3, 0), Expires: time.Now().AddDate(0, 3, 0),

View File

@ -30,7 +30,7 @@ func (h *HttpServer) ManageAppsGet(rw http.ResponseWriter, req *http.Request, _
if err != nil { if err != nil {
return return
} }
appList, err = tx.GetAppList(offset) appList, err = tx.GetAppList(auth.Data.ID, role == database.RoleAdmin, offset)
return return
}) { }) {
return return
@ -72,6 +72,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
action := req.Form.Get("action") action := req.Form.Get("action")
name := req.Form.Get("name") name := req.Form.Get("name")
domain := req.Form.Get("domain") domain := req.Form.Get("domain")
public := req.Form.Has("public")
sso := req.Form.Has("sso") sso := req.Form.Has("sso")
active := req.Form.Has("active") active := req.Form.Has("active")
@ -92,7 +93,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
switch action { switch action {
case "create": case "create":
if h.DbTx(rw, func(tx *database.Tx) error { if h.DbTx(rw, func(tx *database.Tx) error {
return tx.InsertClientApp(name, domain, sso, active, auth.Data.ID) return tx.InsertClientApp(name, domain, public, sso, active, auth.Data.ID)
}) { }) {
return return
} }
@ -102,7 +103,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
if err != nil { if err != nil {
return err 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 return
} }

View File

@ -137,7 +137,7 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
} }
http.SetCookie(rw, &http.Cookie{ http.SetCookie(rw, &http.Cookie{
Name: "login-data", Name: "tulip-login-data",
Path: "/", Path: "/",
MaxAge: -1, MaxAge: -1,
Secure: true, Secure: true,
@ -173,8 +173,8 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
r.POST("/edit/otp", hs.RequireAuthentication(hs.EditOtpPost)) r.POST("/edit/otp", hs.RequireAuthentication(hs.EditOtpPost))
// management pages // management pages
r.GET("/manage/apps", hs.RequireAdminAuthentication(hs.ManageAppsGet)) r.GET("/manage/apps", hs.RequireAuthentication(hs.ManageAppsGet))
r.POST("/manage/apps", hs.RequireAdminAuthentication(hs.ManageAppsPost)) r.POST("/manage/apps", hs.RequireAuthentication(hs.ManageAppsPost))
r.GET("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersGet)) r.GET("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersGet))
r.POST("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersPost)) r.POST("/manage/users", hs.RequireAdminAuthentication(hs.ManageUsersPost))