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

@@ -47,9 +47,9 @@ func TestExtendUsersMigration(t *testing.T) {
if !row.HasSelfSetPassword || row.IsActivated || row.MarketingConsent || row.IsOnboarded {
t.Fatalf("defaults = %+v", row)
}
// Backend links, email verified at, handle, OAuth identities, the three admin migrations, API tokens, user groups,
// jwt blacklist, throttle, then extend-users.
for range 12 {
// Device auth sessions (golem15_user_device_auth_sessions), backend links, email verified at, handle, OAuth
// identities, the three admin migrations, API tokens, user groups, jwt blacklist, throttle, then extend-users.
for range 13 {
if err := m.RollbackLast(); err != nil {
t.Fatal(err)
}
@@ -88,8 +88,9 @@ func TestCreateUserThrottleMigration(t *testing.T) {
t.Fatalf("missing column %s", col)
}
}
// Backend links, email verified at, handle, OAuth identities, the three admin migrations, API tokens, user groups, jwt blacklist, then throttle.
for range 11 {
// Device auth sessions (golem15_user_device_auth_sessions), backend links, email verified at, handle, OAuth
// identities, the three admin migrations, API tokens, user groups, jwt blacklist, then throttle.
for range 12 {
if err := m.RollbackLast(); err != nil {
t.Fatal(err)
}
@@ -120,8 +121,9 @@ func TestCreateJwtBlacklistMigration(t *testing.T) {
t.Fatalf("missing column %s", col)
}
}
// Backend links, email verified at, handle, OAuth identities, the three admin migrations, API tokens, user groups, then jwt blacklist.
for range 10 {
// Device auth sessions (golem15_user_device_auth_sessions), backend links, email verified at, handle, OAuth
// identities, the three admin migrations, API tokens, user groups, then jwt blacklist.
for range 11 {
if err := m.RollbackLast(); err != nil {
t.Fatal(err)
}