feat(cardinal): authenticate game tokens scoped to organization/project - #1004
ryanditjia wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
Review completed against the latest diff
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
eafce1b to
949b9ec
Compare
There was a problem hiding this comment.
0 issues found across 4 files (changes from recent commits).
Shadow auto-approve: would not auto-approve. Auto-approval skipped because cubic reviewed only this push, not the earlier force-push. Comment @cubic review to review the whole pull request.
Re-trigger cubic
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
9ac3b9a to
e2cb5e4
Compare
Shards reach Auth through an internal URL that differs from the public issuer. Signature, audience, and expiry still bind the token to Auth and this project. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
❌ 1 Tests Failed:
View the top 1 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
There was a problem hiding this comment.
1 issue found across 6 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/cardinal/client-integration.mdx">
<violation number="1" location="docs/cardinal/client-integration.mdx:57">
P2: This snippet only handles `AuthNoSavedSessionException`, so a restore failure for any other reason (expired/revoked session, network error) or a failed `SignInAsync` leaves `result.Player` empty and the example proceeds to `client.Player` with no log and no handling. The previous version guarded on `result.Error == null`; check `result.Error` after the restore/sign-in chain and bail out before using `client.Player`.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| if (result.Error is AuthNoSavedSessionException) | ||
| result = await client.SignInAsync(); // or SignInAsGuestAsync() | ||
|
|
||
| if (result.Player is PlayerState.Registered player) |
There was a problem hiding this comment.
P2: This snippet only handles AuthNoSavedSessionException, so a restore failure for any other reason (expired/revoked session, network error) or a failed SignInAsync leaves result.Player empty and the example proceeds to client.Player with no log and no handling. The previous version guarded on result.Error == null; check result.Error after the restore/sign-in chain and bail out before using client.Player.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/cardinal/client-integration.mdx, line 57:
<comment>This snippet only handles `AuthNoSavedSessionException`, so a restore failure for any other reason (expired/revoked session, network error) or a failed `SignInAsync` leaves `result.Player` empty and the example proceeds to `client.Player` with no log and no handling. The previous version guarded on `result.Error == null`; check `result.Error` after the restore/sign-in chain and bail out before using `client.Player`.</comment>
<file context>
@@ -45,20 +45,20 @@ Create a client with a configuration object that specifies the auth URL and regi
+ if (result.Error is AuthNoSavedSessionException)
+ result = await client.SignInAsync(); // or SignInAsGuestAsync()
+
+ if (result.Player is PlayerState.Registered player)
+ Debug.Log($"Signed in as {player.Email} (Player: {player.Id})");
</file context>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="pkg/cardinal/service.go">
<violation number="1" location="pkg/cardinal/service.go:741">
P1: This concatenation breaks valid auth URLs with a trailing slash by requesting `//auth/jwks`, so the shard can fail during authenticator initialization. Preserve trailing-slash normalization before appending the JWKS path.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| func newAuthenticatorArgus(argusAuthURL, organization, project string) (*authenticatorArgus, error) { | ||
| assert.That(argusAuthURL != "", "Should've validated the URL") | ||
|
|
||
| jwksURL := argusAuthURL + "/auth/jwks" |
There was a problem hiding this comment.
P1: This concatenation breaks valid auth URLs with a trailing slash by requesting //auth/jwks, so the shard can fail during authenticator initialization. Preserve trailing-slash normalization before appending the JWKS path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At pkg/cardinal/service.go, line 741:
<comment>This concatenation breaks valid auth URLs with a trailing slash by requesting `//auth/jwks`, so the shard can fail during authenticator initialization. Preserve trailing-slash normalization before appending the JWKS path.</comment>
<file context>
@@ -738,7 +738,7 @@ type authenticatorArgus struct {
assert.That(argusAuthURL != "", "Should've validated the URL")
- jwksURL := strings.TrimRight(argusAuthURL, "/") + "/auth/jwks"
+ jwksURL := argusAuthURL + "/auth/jwks"
client := &http.Client{
Timeout: 3 * time.Second,
</file context>
| jwksURL := argusAuthURL + "/auth/jwks" | |
| jwksURL := strings.TrimRight(argusAuthURL, "/") + "/auth/jwks" |
…DK API Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/cardinal/client-integration.mdx">
<violation number="1" location="docs/cardinal/client-integration.mdx:53">
P3: The startup sample never handles the `SignedOut` case, even though the paragraph above says `AuthStatus` tells the game what it can do. A fresh install (no saved login) follows this snippet and gets no render and no direction to sign in; a failed `RefreshAuthAsync` is likewise silently swallowed. Render the `SignedOut` state and make refresh failure visible so readers following the sample end up authenticated or with a clear next step.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| client.AuthChanged += Render; | ||
| Render(); | ||
| if (client.AuthStatus == AuthStatus.NeedsRefresh) | ||
| await client.RefreshAuthAsync(); |
There was a problem hiding this comment.
P3: The startup sample never handles the SignedOut case, even though the paragraph above says AuthStatus tells the game what it can do. A fresh install (no saved login) follows this snippet and gets no render and no direction to sign in; a failed RefreshAuthAsync is likewise silently swallowed. Render the SignedOut state and make refresh failure visible so readers following the sample end up authenticated or with a clear next step.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/cardinal/client-integration.mdx, line 53:
<comment>The startup sample never handles the `SignedOut` case, even though the paragraph above says `AuthStatus` tells the game what it can do. A fresh install (no saved login) follows this snippet and gets no render and no direction to sign in; a failed `RefreshAuthAsync` is likewise silently swallowed. Render the `SignedOut` state and make refresh failure visible so readers following the sample end up authenticated or with a clear next step.</comment>
<file context>
@@ -45,20 +45,26 @@ Create a client with a configuration object that specifies the auth URL and regi
- var result = await client.RestoreSessionAsync();
- if (result.Error is AuthNoSavedSessionException)
- result = await client.SignInAsync(); // or SignInAsGuestAsync()
+ client.AuthChanged += Render;
+ Render();
+ if (client.AuthStatus == AuthStatus.NeedsRefresh)
</file context>
| client.AuthChanged += Render; | |
| Render(); | |
| if (client.AuthStatus == AuthStatus.NeedsRefresh) | |
| await client.RefreshAuthAsync(); | |
| client.AuthChanged += Render; | |
| Render(); | |
| if (client.AuthStatus == AuthStatus.SignedOut) | |
| Debug.Log("No saved login — sign in to continue."); | |
| else if (client.AuthStatus == AuthStatus.NeedsRefresh) | |
| await client.RefreshAuthAsync(); |

Cardinal authenticates commands and event subscriptions with the stable player ID in a game token's
sub. Saving guest progress to a registered account therefore keeps the identity used by game systems.Validation checks the signature, issuer, expiration, and audience against the shard's existing
organization/projectconfiguration. Tokens for another game and ordinary account tokens are rejected. Development clients useX-Player-ID. Game systems continue receiving the player ID throughcmd.Persona; wire-format persona cleanup is separate. Expiration is checked when requests and streams start.Deploy Auth #778 before this engine update.
Validation:
go test ./pkg/cardinalpassed.