mirror of
https://github.com/1f349/lavender.git
synced 2024-12-22 07:34:06 +00:00
Allow specific permissions to be sent to authorized clients and use JWT access tokens
This commit is contained in:
parent
05b19e6bf2
commit
c6d64e5d81
@ -15,7 +15,7 @@ type User struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ClientInfoDbOutput struct {
|
type ClientInfoDbOutput struct {
|
||||||
Sub, Name, Secret, Domain, Owner string
|
Sub, Name, Secret, Domain, Owner, Perms string
|
||||||
Public, SSO, Active bool
|
Public, SSO, Active bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,3 +37,6 @@ func (c *ClientInfoDbOutput) IsSSO() bool { return c.SSO }
|
|||||||
|
|
||||||
// IsActive is an extra field for the app manager to get the active state
|
// IsActive is an extra field for the app manager to get the active state
|
||||||
func (c *ClientInfoDbOutput) IsActive() bool { return c.Active }
|
func (c *ClientInfoDbOutput) IsActive() bool { return c.Active }
|
||||||
|
|
||||||
|
// UsePerms is an extra field for the userinfo handler to return user permissions matching the requested values
|
||||||
|
func (c *ClientInfoDbOutput) UsePerms() string { return c.Perms }
|
||||||
|
@ -18,6 +18,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,
|
||||||
|
perms TEXT NOT NULL,
|
||||||
public INTEGER,
|
public INTEGER,
|
||||||
sso INTEGER,
|
sso INTEGER,
|
||||||
active INTEGER DEFAULT 1,
|
active INTEGER DEFAULT 1,
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/1f349/lavender/password"
|
"github.com/1f349/lavender/password"
|
||||||
"github.com/go-oauth2/oauth2/v4"
|
"github.com/go-oauth2/oauth2/v4"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -65,8 +66,8 @@ func (t *Tx) GetUserEmail(sub string) (string, 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, public, sso, active FROM client_store WHERE subject = ? LIMIT 1`, sub)
|
row := t.tx.QueryRow(`SELECT secret, name, domain, perms, 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)
|
err := row.Scan(&u.Secret, &u.Name, &u.Domain, &u.Perms, &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")
|
||||||
@ -76,14 +77,14 @@ func (t *Tx) GetClientInfo(sub string) (oauth2.ClientInfo, error) {
|
|||||||
|
|
||||||
func (t *Tx) GetAppList(owner string, admin bool, offset int) ([]ClientInfoDbOutput, error) {
|
func (t *Tx) GetAppList(owner string, admin bool, offset int) ([]ClientInfoDbOutput, error) {
|
||||||
var u []ClientInfoDbOutput
|
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, admin, offset)
|
row, err := t.tx.Query(`SELECT subject, name, domain, owner, perms, public, sso, active FROM client_store WHERE owner = ? OR ? = 1 LIMIT 25 OFFSET ?`, owner, 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.Public, &a.SSO, &a.Active)
|
err := row.Scan(&a.Sub, &a.Name, &a.Domain, &a.Owner, &a.Perms, &a.Public, &a.SSO, &a.Active)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -92,18 +93,19 @@ func (t *Tx) GetAppList(owner string, admin bool, offset int) ([]ClientInfoDbOut
|
|||||||
return u, row.Err()
|
return u, row.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tx) InsertClientApp(name, domain string, public, sso, active bool, owner string) error {
|
func (t *Tx) InsertClientApp(name, domain, owner, perms string, public, sso, active bool) 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, public, sso, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, u.String(), name, secret, domain, owner, public, sso, active)
|
_, err = t.tx.Exec(`INSERT INTO client_store (subject, name, secret, domain, owner, perms, public, sso, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, u.String(), name, secret, domain, owner, perms, public, sso, active)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tx) UpdateClientApp(subject uuid.UUID, owner string, name, domain string, public, sso, active bool) error {
|
func (t *Tx) UpdateClientApp(subject uuid.UUID, owner, name, domain, perms string, hasPerms, 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)
|
log.Println(hasPerms, perms)
|
||||||
|
_, err := t.tx.Exec(`UPDATE client_store SET name = ?, domain = ?, perms = CASE WHEN ? = true THEN ? ELSE perms END, public = ?, sso = ?, active = ? WHERE subject = ? AND owner = ?`, name, domain, hasPerms, perms, public, sso, active, subject.String(), owner)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
go.mod
3
go.mod
@ -10,6 +10,7 @@ require (
|
|||||||
github.com/MrMelon54/exit-reload v0.0.1
|
github.com/MrMelon54/exit-reload v0.0.1
|
||||||
github.com/go-oauth2/oauth2/v4 v4.5.2
|
github.com/go-oauth2/oauth2/v4 v4.5.2
|
||||||
github.com/go-session/session v3.1.2+incompatible
|
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/subcommands v1.2.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
@ -20,9 +21,9 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/MrMelon54/rescheduler v0.0.2 // indirect
|
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/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+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/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -15,6 +15,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/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 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
@ -58,6 +58,12 @@
|
|||||||
<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>
|
||||||
|
{{if .IsAdmin}}
|
||||||
|
<div>
|
||||||
|
<label for="field_perms">Perms:</label>
|
||||||
|
<input type="text" name="perms" id="field_perms" value="{{.Edit.Perms}}" required/>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<div>
|
<div>
|
||||||
<label for="field_public">Public: <input type="checkbox" name="public" id="field_public" {{if .Edit.Public}}checked{{end}}/></label>
|
<label for="field_public">Public: <input type="checkbox" name="public" id="field_public" {{if .Edit.Public}}checked{{end}}/></label>
|
||||||
</div>
|
</div>
|
||||||
@ -86,6 +92,7 @@
|
|||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Domain</th>
|
<th>Domain</th>
|
||||||
|
<th>Perms</th>
|
||||||
<th>SSO</th>
|
<th>SSO</th>
|
||||||
<th>Active</th>
|
<th>Active</th>
|
||||||
<th>Owner</th>
|
<th>Owner</th>
|
||||||
@ -98,6 +105,7 @@
|
|||||||
<td>{{.Sub}}</td>
|
<td>{{.Sub}}</td>
|
||||||
<td>{{.Name}}</td>
|
<td>{{.Name}}</td>
|
||||||
<td>{{.Domain}}</td>
|
<td>{{.Domain}}</td>
|
||||||
|
<td>{{.Perms}}</td>
|
||||||
<td>{{.SSO}}</td>
|
<td>{{.SSO}}</td>
|
||||||
<td>{{.Active}}</td>
|
<td>{{.Active}}</td>
|
||||||
<td>{{.Owner}}</td>
|
<td>{{.Owner}}</td>
|
||||||
@ -132,6 +140,12 @@
|
|||||||
<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>
|
||||||
|
{{if .IsAdmin}}
|
||||||
|
<div>
|
||||||
|
<label for="field_perms">Perms:</label>
|
||||||
|
<input type="text" name="perms" id="field_perms" required/>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<div>
|
<div>
|
||||||
<label for="field_public">Public: <input type="checkbox" name="public" id="field_public"/></label>
|
<label for="field_public">Public: <input type="checkbox" name="public" id="field_public"/></label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,7 +69,7 @@ func (h *HttpServer) OptionalAuthentication(next UserHandler) httprouter.Handle
|
|||||||
}
|
}
|
||||||
if auth.IsGuest() {
|
if auth.IsGuest() {
|
||||||
// if this fails internally it just sees the user as logged out
|
// if this fails internally it just sees the user as logged out
|
||||||
h.readLoginDataCookie(rw, req, &auth)
|
h.readLoginDataCookie(req, &auth)
|
||||||
}
|
}
|
||||||
next(rw, req, params, auth)
|
next(rw, req, params, auth)
|
||||||
}
|
}
|
||||||
|
10
server/db.go
10
server/db.go
@ -31,3 +31,13 @@ func (h *HttpServer) DbTx(rw http.ResponseWriter, action func(tx *database.Tx) e
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) DbTxRaw(action func(tx *database.Tx) error) bool {
|
||||||
|
return h.DbTx(&fakeRW{}, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeRW struct{}
|
||||||
|
|
||||||
|
func (f *fakeRW) Header() http.Header { return http.Header{} }
|
||||||
|
func (f *fakeRW) Write(b []byte) (int, error) { return len(b), nil }
|
||||||
|
func (f *fakeRW) WriteHeader(statusCode int) {}
|
||||||
|
@ -7,9 +7,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {
|
func (h *HttpServer) Home(rw http.ResponseWriter, _ *http.Request, _ httprouter.Params, auth UserAuth) {
|
||||||
rw.Header().Set("Content-Type", "text/html")
|
rw.Header().Set("Content-Type", "text/html")
|
||||||
rw.WriteHeader(http.StatusOK)
|
|
||||||
if auth.IsGuest() {
|
if auth.IsGuest() {
|
||||||
pages.RenderPageTemplate(rw, "index-guest", map[string]any{
|
pages.RenderPageTemplate(rw, "index-guest", map[string]any{
|
||||||
"ServiceName": h.conf.ServiceName,
|
"ServiceName": h.conf.ServiceName,
|
||||||
|
57
server/jwt.go
Normal file
57
server/jwt.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"github.com/1f349/lavender/database"
|
||||||
|
"github.com/1f349/mjwt"
|
||||||
|
"github.com/1f349/mjwt/auth"
|
||||||
|
"github.com/1f349/mjwt/claims"
|
||||||
|
"github.com/go-oauth2/oauth2/v4"
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JWTAccessGenerate struct {
|
||||||
|
signer mjwt.Signer
|
||||||
|
db *database.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJWTAccessGenerate(signer mjwt.Signer, db *database.DB) *JWTAccessGenerate {
|
||||||
|
return &JWTAccessGenerate{signer, db}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ oauth2.AccessGenerate = &JWTAccessGenerate{}
|
||||||
|
|
||||||
|
func (j *JWTAccessGenerate) Token(ctx context.Context, data *oauth2.GenerateBasic, isGenRefresh bool) (access, refresh string, err error) {
|
||||||
|
beginCtx, err := j.db.BeginCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
roles, err := beginCtx.GetUserRoles(data.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
beginCtx.Rollback()
|
||||||
|
|
||||||
|
ps := claims.NewPermStorage()
|
||||||
|
ForEachRole(data.Client.(interface{ UsePerms() string }).UsePerms(), func(role string) {
|
||||||
|
if HasRole(roles, role) {
|
||||||
|
ps.Set(role)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
access, err = j.signer.GenerateJwt(data.UserID, "", jwt.ClaimStrings{data.TokenInfo.GetClientID()}, data.TokenInfo.GetAccessExpiresIn(), auth.AccessTokenClaims{
|
||||||
|
Perms: ps,
|
||||||
|
})
|
||||||
|
|
||||||
|
if isGenRefresh {
|
||||||
|
t := uuid.NewHash(sha256.New(), uuid.New(), []byte(access), 5).String()
|
||||||
|
refresh = base64.URLEncoding.EncodeToString([]byte(t))
|
||||||
|
refresh = strings.ToUpper(strings.TrimRight(refresh, "="))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
@ -1,13 +1,12 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/base64"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -129,7 +128,7 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _
|
|||||||
|
|
||||||
// only continues if the above tx succeeds
|
// only continues if the above tx succeeds
|
||||||
auth.Data = sessionData
|
auth.Data = sessionData
|
||||||
if auth.SaveSessionData() != nil {
|
if err := auth.SaveSessionData(); err != nil {
|
||||||
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
|
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -140,8 +139,8 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.setLoginDataCookie(rw, auth.Data.ID, token) {
|
if h.setLoginDataCookie(rw, auth.Data.ID) {
|
||||||
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(rw, "Failed to save login cookie", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if flowState.redirect != "" {
|
if flowState.redirect != "" {
|
||||||
@ -150,22 +149,15 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _
|
|||||||
h.SafeRedirect(rw, req)
|
h.SafeRedirect(rw, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId string, token *oauth2.Token) bool {
|
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId string) bool {
|
||||||
buf := new(bytes.Buffer)
|
encData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, h.signingKey.PublicKey(), []byte(userId), []byte("lavender-login-data"))
|
||||||
buf.WriteString(userId)
|
|
||||||
buf.WriteByte(0)
|
|
||||||
err := json.NewEncoder(buf).Encode(token)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true
|
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{
|
http.SetCookie(rw, &http.Cookie{
|
||||||
Name: "lavender-login-data",
|
Name: "lavender-login-data",
|
||||||
Value: encryptedString,
|
Value: hex.EncodeToString(encData),
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Expires: time.Now().AddDate(0, 3, 0),
|
Expires: time.Now().AddDate(0, 3, 0),
|
||||||
Secure: true,
|
Secure: true,
|
||||||
@ -174,30 +166,25 @@ func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId string, t
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) readLoginDataCookie(rw http.ResponseWriter, req *http.Request, u *UserAuth) {
|
func (h *HttpServer) readLoginDataCookie(req *http.Request, u *UserAuth) {
|
||||||
loginCookie, err := req.Cookie("lavender-login-data")
|
loginCookie, err := req.Cookie("lavender-login-data")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
decryptedBytes, err := base64.RawStdEncoding.DecodeString(loginCookie.Value)
|
hexData, err := hex.DecodeString(loginCookie.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, h.signingKey.PrivateKey(), decryptedBytes, []byte("lavender-login-data"))
|
decData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, h.signingKey.PrivateKey(), hexData, []byte("lavender-login-data"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.NewBuffer(decryptedData)
|
userId := string(decData)
|
||||||
userId, err := buf.ReadString(0)
|
var token oauth2.Token
|
||||||
if err != nil {
|
if h.DbTxRaw(func(tx *database.Tx) error {
|
||||||
return
|
return tx.GetUserToken(userId, &token.AccessToken, &token.RefreshToken, &token.Expiry)
|
||||||
}
|
}) {
|
||||||
userId = strings.TrimSuffix(userId, "\x00")
|
|
||||||
|
|
||||||
var token *oauth2.Token
|
|
||||||
err = json.NewDecoder(buf).Decode(&token)
|
|
||||||
if err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,11 +193,7 @@ func (h *HttpServer) readLoginDataCookie(rw http.ResponseWriter, req *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
u.Data, err = h.fetchUserInfo(sso, token)
|
u.Data, _ = h.fetchUserInfo(sso, &token)
|
||||||
if err != nil {
|
|
||||||
http.Error(rw, "Failed to fetch user info", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) fetchUserInfo(sso *issuer.WellKnownOIDC, token *oauth2.Token) (SessionData, error) {
|
func (h *HttpServer) fetchUserInfo(sso *issuer.WellKnownOIDC, token *oauth2.Token) (SessionData, error) {
|
||||||
|
@ -72,11 +72,12 @@ 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")
|
||||||
|
hasPerms := req.Form.Has("perms")
|
||||||
public := req.Form.Has("public")
|
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")
|
||||||
|
|
||||||
if sso {
|
if sso || hasPerms {
|
||||||
var roles string
|
var roles string
|
||||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||||
roles, err = tx.GetUserRoles(auth.Data.ID)
|
roles, err = tx.GetUserRoles(auth.Data.ID)
|
||||||
@ -85,15 +86,19 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !HasRole(roles, "lavender:admin") {
|
if !HasRole(roles, "lavender:admin") {
|
||||||
http.Error(rw, "400 Bad Request: Only admin users can create SSO client applications", http.StatusBadRequest)
|
http.Error(rw, "400 Bad Request: Only admin users can create SSO client applications or edit required permissions", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var perms string
|
||||||
|
if hasPerms {
|
||||||
|
perms = req.Form.Get("perms")
|
||||||
|
}
|
||||||
|
|
||||||
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, public, sso, active, auth.Data.ID)
|
return tx.InsertClientApp(name, domain, auth.Data.ID, perms, public, sso, active)
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -103,7 +108,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, public, sso, active)
|
return tx.UpdateClientApp(sub, auth.Data.ID, name, domain, perms, hasPerms, public, sso, active)
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -15,3 +15,11 @@ func HasRole(roles, test string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ForEachRole(roles string, next func(role string)) {
|
||||||
|
sc := bufio.NewScanner(strings.NewReader(roles))
|
||||||
|
sc.Split(bufio.ScanWords)
|
||||||
|
for sc.Scan() {
|
||||||
|
next(sc.Text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,13 +14,12 @@ import (
|
|||||||
"github.com/1f349/lavender/theme"
|
"github.com/1f349/lavender/theme"
|
||||||
"github.com/1f349/mjwt"
|
"github.com/1f349/mjwt"
|
||||||
"github.com/go-oauth2/oauth2/v4/errors"
|
"github.com/go-oauth2/oauth2/v4/errors"
|
||||||
"github.com/go-oauth2/oauth2/v4/generates"
|
|
||||||
"github.com/go-oauth2/oauth2/v4/manage"
|
"github.com/go-oauth2/oauth2/v4/manage"
|
||||||
"github.com/go-oauth2/oauth2/v4/server"
|
"github.com/go-oauth2/oauth2/v4/server"
|
||||||
"github.com/go-oauth2/oauth2/v4/store"
|
"github.com/go-oauth2/oauth2/v4/store"
|
||||||
"github.com/go-session/session"
|
"github.com/go-session/session"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
oauth22 "golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -84,7 +83,7 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
|
|||||||
|
|
||||||
oauthManager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
|
oauthManager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
|
||||||
oauthManager.MustTokenStorage(store.NewMemoryTokenStore())
|
oauthManager.MustTokenStorage(store.NewMemoryTokenStore())
|
||||||
oauthManager.MapAccessGenerate(generates.NewAccessGenerate())
|
oauthManager.MapAccessGenerate(NewJWTAccessGenerate(hs.signingKey, db))
|
||||||
oauthManager.MapClientStorage(clientStore.New(db))
|
oauthManager.MapClientStorage(clientStore.New(db))
|
||||||
|
|
||||||
oauthSrv.SetResponseErrorHandler(func(re *errors.Response) {
|
oauthSrv.SetResponseErrorHandler(func(re *errors.Response) {
|
||||||
@ -194,7 +193,7 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientToken oauth22.Token
|
var clientToken oauth2.Token
|
||||||
if hs.DbTx(rw, func(tx *database.Tx) error {
|
if hs.DbTx(rw, func(tx *database.Tx) error {
|
||||||
return tx.GetUserToken(userId, &clientToken.AccessToken, &clientToken.RefreshToken, &clientToken.Expiry)
|
return tx.GetUserToken(userId, &clientToken.AccessToken, &clientToken.RefreshToken, &clientToken.Expiry)
|
||||||
}) {
|
}) {
|
||||||
|
@ -8,12 +8,28 @@
|
|||||||
|
|
||||||
POP2.init(ssoService + "/authorize", "f4cdb93d-fe28-427b-b037-f03f44c86a16", "openid profile age", 500, 600);
|
POP2.init(ssoService + "/authorize", "f4cdb93d-fe28-427b-b037-f03f44c86a16", "openid profile age", 500, 600);
|
||||||
|
|
||||||
|
window.addEventListener("load", function () {
|
||||||
|
doThisThing(false);
|
||||||
|
})
|
||||||
|
|
||||||
function updateTokenInfo(data) {
|
function updateTokenInfo(data) {
|
||||||
document.getElementById("someTextArea").textContent = JSON.stringify(data, null, 2);
|
document.getElementById("someTextArea").textContent = JSON.stringify(data, null, 2);
|
||||||
|
POP2.getToken(function (x) {
|
||||||
|
document.getElementById("tokenValues").textContent = JSON.stringify(parseJwt(x), null, 2);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function doThisThing() {
|
function parseJwt(token) {
|
||||||
POP2.clientRequest(ssoService + "/userinfo", {}, true).then(function (x) {
|
const base64Url = token.split('.')[1];
|
||||||
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function (c) {
|
||||||
|
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||||
|
}).join(''));
|
||||||
|
return JSON.parse(jsonPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doThisThing(popup) {
|
||||||
|
POP2.clientRequest(ssoService + "/userinfo", {}, popup).then(function (x) {
|
||||||
return x.json();
|
return x.json();
|
||||||
}).then(function (x) {
|
}).then(function (x) {
|
||||||
updateTokenInfo(x);
|
updateTokenInfo(x);
|
||||||
@ -44,7 +60,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div>
|
<div>
|
||||||
<button onclick="doThisThing();">Login</button>
|
<button onclick="doThisThing(true);">Login</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap: 2em;">
|
<div style="display:flex; gap: 2em;">
|
||||||
<div>
|
<div>
|
||||||
|
@ -104,14 +104,10 @@
|
|||||||
callbackWaitForToken = undefined;
|
callbackWaitForToken = undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// boolean, indicate logged in or not
|
|
||||||
isLoggedIn: function () {
|
|
||||||
return !!access_token;
|
|
||||||
},
|
|
||||||
// pass the access token to callback
|
// pass the access token to callback
|
||||||
// if not logged in this triggers login popup;
|
// if not logged in this triggers login popup;
|
||||||
// use isLoggedIn to check login first to prevent popup blocker
|
// use isLoggedIn to check login first to prevent popup blocker
|
||||||
getToken: function (callback) {
|
getToken: function (callback, popup = true) {
|
||||||
if (!client_id || !redirect_uri || !scope) {
|
if (!client_id || !redirect_uri || !scope) {
|
||||||
alert('You need init() first. Check the program flow.');
|
alert('You need init() first. Check the program flow.');
|
||||||
return false;
|
return false;
|
||||||
@ -128,8 +124,10 @@
|
|||||||
w_width,
|
w_width,
|
||||||
w_height
|
w_height
|
||||||
);
|
);
|
||||||
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return callback(access_token);
|
callback(access_token);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clientRequest: function (resource, options, refresh = false) {
|
clientRequest: function (resource, options, refresh = false) {
|
||||||
@ -159,8 +157,10 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!refresh) return sendRequest();
|
if (!refresh) {
|
||||||
else {
|
if (!access_token) return Promise.reject("missing access token");
|
||||||
|
return sendRequest();
|
||||||
|
} else {
|
||||||
return new Promise(function (res, rej) {
|
return new Promise(function (res, rej) {
|
||||||
sendRequest().then(function (x) {
|
sendRequest().then(function (x) {
|
||||||
res(x);
|
res(x);
|
||||||
|
Loading…
Reference in New Issue
Block a user