Skip to content

fix(jwt): app-scoped user account fallback + HMAC-parity response shape with FK expansion#88

Merged
kennethphough merged 3 commits into
masterfrom
fix/jwt-app-scoped-account
May 22, 2026
Merged

fix(jwt): app-scoped user account fallback + HMAC-parity response shape with FK expansion#88
kennethphough merged 3 commits into
masterfrom
fix/jwt-app-scoped-account

Conversation

@kennethphough

Copy link
Copy Markdown
Member

Summary

Three related fixes for JWT login on apps with a custom user_model. Verified end-to-end on dev (device 19) via dev-branch composer pinning before tagging.

Commits

  1. fix(jwt): fall back to app->kyte_account for app-scoped users — STEP3 of login required user->kyte_account != 0, but app-scoped users have kyte_application FK instead. Fall back to app->kyte_account when user lacks one.
  2. feat(jwt): mirror HMAC session response shape (uid, account_id, data) — JWT response only had tokens. HMAC returns uid/account_id/data, which customer frontends consume. Match the shape (excluding HMAC-only kyte_pub/iden/num/session/token fields).
  3. feat(jwt): expand FK fields in login response data (SESSION_RETURN_FK parity) — HMAC SessionController expands FKs (user.org{id:2, org_type:'LP', ...}). JWT was returning raw FK ints. Recursively expand bounded at depth 3.

Test plan

  • All 12 JwtEndpointTest tests pass
  • End-to-end JWT login verified on dev against a customer app with custom user_model='User', app-scoped FK org, and frontend code consuming user.org.org_type
  • Branch pinned via composer dev-branch on dev (dev-fix/jwt-app-scoped-account as 4.4.x-dev) — only host that received changes pre-merge

Known follow-up

FK expansion is N+1 per login. Tempo card #171 for optimization (eager-loading hook OR per-user cache for access-token TTL).

Patch v4.4.5. No schema changes.

🤖 Generated with Claude Code

kennethphough and others added 3 commits May 22, 2026 12:25
JwtEndpoint::login required user->kyte_account != 0 to proceed. That
holds for the default KyteUser model but NOT for app-scoped User
models created via Shipyard DataModel — those users belong to the
Application, and the Application carries the kyte_account FK.

Result: login on apps with custom user_model returned 401 even for
valid credentials. STEP3 (accountId === 0) bailed before token
issuance, despite STEP1 user retrieve + STEP2 password_verify both
passing.

Fix: if user->kyte_account is missing/0 and app context is set, fall
back to app->kyte_account. Mirrors how HMAC derives account context
from the app for session-on-app flows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JWT /jwt/login was returning only the tokens. HMAC /Session also
returns uid, account_id, and data (user object array with protected
fields stripped) — customer apps consuming the response (e.g.
session.data[0].organization_type) silently fail under JWT because
the payload was missing.

Add to JWT login response:
  - uid:        user.id
  - account_id: account.id
  - data:       user data with protected fields ('' replaced),
                wrapped per USE_SESSION_MAP (array vs object) to
                match HMAC SessionController::new behavior

Intentionally NOT included:
  - kyte_pub / kyte_iden / kyte_num — HMAC-only API handoff creds
    (JWT has no handoff concept; access_token is the credential)
  - session / token — HMAC-only sessionToken / txToken
  - session_created — derivable from access token iat claim

New userToArray() helper standalone-replicates the protected-field
stripping from ModelController::getObject without requiring a
controller context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… parity)

HMAC SessionController sets getFKTables = SESSION_RETURN_FK (default
true), causing getObject() to recursively expand FK integers into
full nested objects. JWT userToArray was returning raw FK ints, so
frontend code like 'user.org.org_type' silently failed (org was '2',
not {id:2, org_type:'LP'}).

Add FK expansion to userToArray:
  - Walk model struct
  - For fields with 'fk' definition + non-empty value, retrieve the
    referenced ModelObject and recursively serialize it
  - Bounded at depth 3 to prevent runaway recursion on cyclic FKs

Respects SESSION_RETURN_FK constant (defaults true, can be disabled
per-deployment). retrieve() handles dbswitch automatically based on
the FK model's appId, so app-scoped + default-scoped FKs both work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kennethphough
kennethphough merged commit feb5f60 into master May 22, 2026
4 checks passed
@kennethphough
kennethphough deleted the fix/jwt-app-scoped-account branch May 22, 2026 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant