feat: device authorization by QR or short code

PHP's device flow on golem15_user_device_auth_sessions (PHP's columns,
the token stored as its SHA-256, plus jwt_jti): POST /devices/initiate
answers a 64-character token, an XXXX-XXXX short code, the configurable
auth_url (golem15.user.device_auth.auth_url) and the expiry; a signed-in
user authorizes it with POST /devices/authorize (short_code or token,
one guarded pending-to-confirmed UPDATE); the device's first poll of
GET /devices/status/{token} after that receives a session JWT through one
guarded confirmed-to-used UPDATE, so exactly one poll gets it. Used and
revoked sessions never turn into expired; each initiate deletes a bounded
batch of stale sessions.
This commit is contained in:
2026-10-26 10:14:48 +01:00
parent 3d4c8d2160
commit 4e3e5b05b5
13 changed files with 1036 additions and 24 deletions

View File

@@ -1071,12 +1071,18 @@ func apiArray(ctx context.Context, app *backpack.App, user *models.User) (map[st
}
func mintFor(app *backpack.App, secret string, user *models.User, issuer string) (string, error) {
token, _, err := bouncer.Mint(secret, strconv.FormatUint(uint64(user.ID), 10), issuer, sessionTTL(app))
return token, err
}
// sessionTTL is the lifetime of a session JWT: golem15.user.jwt.ttl
// minutes, default 60.
func sessionTTL(app *backpack.App) time.Duration {
ttl := 60 * time.Minute
if app != nil && app.Config != nil && app.Config.Int("golem15.user.jwt.ttl") > 0 {
ttl = time.Duration(app.Config.Int("golem15.user.jwt.ttl")) * time.Minute
}
token, _, err := bouncer.Mint(secret, strconv.FormatUint(uint64(user.ID), 10), issuer, ttl)
return token, err
return ttl
}
// RefreshTTL is the refresh window of a session token: