Allow specific permissions to be sent to authorized clients and use JWT access tokens

This commit is contained in:
Melon 2024-02-10 16:23:50 +00:00
parent 05b19e6bf2
commit c6d64e5d81
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
16 changed files with 167 additions and 67 deletions

View File

@ -15,8 +15,8 @@ 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
} }
var _ oauth2.ClientInfo = &ClientInfoDbOutput{} var _ oauth2.ClientInfo = &ClientInfoDbOutput{}
@ -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 }

View File

@ -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,

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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>

View File

@ -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)
} }

View File

@ -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) {}

View File

@ -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
View 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
}

View File

@ -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) {

View File

@ -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
} }

View File

@ -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())
}
}

View File

@ -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)
}) { }) {

View File

@ -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>

View File

@ -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);