-
Notifications
You must be signed in to change notification settings - Fork 20
fix(auth): deliver the session cookie to the Owletto extension iframe (CHIPS) #1092
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
packages/server/src/auth/__tests__/exchange-token-cookie.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { beforeEach, describe, expect, it } from 'vitest'; | ||
| import { cleanupTestDatabase } from '../../__tests__/setup/test-db'; | ||
| import { | ||
| addUserToOrganization, | ||
| createTestAccessToken, | ||
| createTestOAuthClient, | ||
| createTestOrganization, | ||
| createTestPAT, | ||
| createTestUser, | ||
| } from '../../__tests__/setup/test-fixtures'; | ||
| import { get, postForm } from '../../__tests__/setup/test-helpers'; | ||
|
|
||
| // The Owletto extension embeds owletto-web as a cross-site iframe (top-level | ||
| // chrome-extension://). Lax cookies are withheld there, so the deep-link cookie | ||
| // posture differs by entry point: the extension iframe (POST, set inside its | ||
| // own partition) needs CHIPS Partitioned; SameSite=None; the CLI/menu-bar | ||
| // first-party deep-link (GET, top-level tab) keeps Lax. See auth/routes.ts. | ||
| describe('exchange-token cookie posture', () => { | ||
| beforeEach(async () => { | ||
| await cleanupTestDatabase(); | ||
| }); | ||
|
|
||
| async function patForNewUser(slug: string, email: string): Promise<string> { | ||
| const org = await createTestOrganization({ slug }); | ||
| const user = await createTestUser({ email }); | ||
| await addUserToOrganization(user.id, org.id, 'owner'); | ||
| const { token } = await createTestPAT(user.id, org.id, { scope: 'profile:read' }); | ||
| return token; | ||
| } | ||
|
|
||
| it('POST mints a CHIPS partitioned cross-site cookie (extension iframe)', async () => { | ||
| const token = await patForNewUser('xt-post-org', 'xt-post@test.example.com'); | ||
| const res = await postForm('/api/exchange-token', { token, next: '/#worker=abc' }); | ||
|
|
||
| expect(res.status).toBe(302); | ||
| expect(res.headers.get('location')).toBe('/#worker=abc'); | ||
| const cookie = res.headers.get('set-cookie') ?? ''; | ||
| expect(cookie).toMatch(/SameSite=None/i); | ||
| expect(cookie).toMatch(/Secure/i); | ||
| expect(cookie).toMatch(/Partitioned/i); | ||
| }); | ||
|
|
||
| it('POST resolves an OAuth device-code access token (cloud pairing path)', async () => { | ||
| // The extension's cloud (OAuth device-code) pairing stores an oauth_tokens | ||
| // access token, NOT an owl_pat_ PAT — exchange-token must resolve it or the | ||
| // cloud iframe 401s and renders signed-out. | ||
| const org = await createTestOrganization({ slug: 'xt-oauth-org' }); | ||
| const user = await createTestUser({ email: 'xt-oauth@test.example.com' }); | ||
| await addUserToOrganization(user.id, org.id, 'owner'); | ||
| const client = await createTestOAuthClient(); | ||
| const { token } = await createTestAccessToken(user.id, org.id, client.client_id, { | ||
| scope: 'profile:read', | ||
| }); | ||
| expect(token.startsWith('owl_pat_')).toBe(false); // genuinely an OAuth access token | ||
|
|
||
| const res = await postForm('/api/exchange-token', { token, next: '/' }); | ||
| expect(res.status).toBe(302); | ||
| expect(res.headers.get('set-cookie') ?? '').toMatch(/Partitioned/i); | ||
| }); | ||
|
|
||
| it('GET keeps a first-party Lax cookie — never Partitioned/None (CLI/menu-bar)', async () => { | ||
| const token = await patForNewUser('xt-get-org', 'xt-get@test.example.com'); | ||
| const res = await get(`/api/exchange-token?token=${encodeURIComponent(token)}&next=/`); | ||
|
|
||
| expect(res.status).toBe(302); | ||
| const cookie = res.headers.get('set-cookie') ?? ''; | ||
| expect(cookie).toMatch(/SameSite=Lax/i); | ||
| expect(cookie).not.toMatch(/Partitioned/i); | ||
| expect(cookie).not.toMatch(/SameSite=None/i); | ||
| }); | ||
|
|
||
| it('rejects a missing or invalid token', async () => { | ||
| expect((await postForm('/api/exchange-token', { next: '/' })).status).toBe(400); | ||
| expect((await postForm('/api/exchange-token', { token: 'owl_pat_nope', next: '/' })).status).toBe(401); | ||
| }); | ||
|
|
||
| it('serves the in-iframe bootstrap page (POSTs the token, strips the fragment)', async () => { | ||
| const res = await get('/api/extension-bootstrap'); | ||
| expect(res.status).toBe(200); | ||
| expect(res.headers.get('content-type') ?? '').toMatch(/text\/html/i); | ||
| const html = await res.text(); | ||
| expect(html).toContain('/api/exchange-token'); | ||
| expect(html).toContain('location.hash'); | ||
| expect(html).toContain('replaceState'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't let arbitrary OAuth bearers mint full web sessions.
This path keeps only
userIdfromverifyAccessToken()and then creates a 7-day Better Auth session, so a scoped OAuth access token can be upgraded into an unrestricted browser login. The new test coverage even proves a generic client token with onlyprofile:readis accepted. Please restrict this exchange to the Owletto pairing client, a dedicated scope, or a single-use exchange credential instead.🤖 Prompt for AI Agents