-
Notifications
You must be signed in to change notification settings - Fork 1
Extract authentication business logic and remove deprecated code #6
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 23 commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
3449cc0
properly capitalize sameSite options to fix Safari's strict cookie is…
nicknisi 7537cf2
infer secure flag from redirectUri if not explicitly set
nicknisi 2c4f03f
fix
nicknisi 4dfdd6a
add AuthKitCore class
nicknisi a590176
add AuthOperations class
nicknisi d9b25f4
add AuthService
nicknisi 5179661
update factory
nicknisi 4e9bd5e
wip
nicknisi 95189f7
add tests
nicknisi b5404f2
convert to toolkit from framework
nicknisi fafd0fb
update deps
nicknisi f43ace0
remove old/deprecated code
nicknisi 6e3f0f7
update README
nicknisi 2beddec
chore: bump version to 0.2.0-beta.0
nicknisi 5234159
refactor: reframe AuthService as one orchestration option, not recomm…
nicknisi c55b00f
formatting
nicknisi 19497e2
remove file
nicknisi 2645f3c
refactor: drop toolkit framing, be honest about architecture
nicknisi b802f54
remove markdown file
nicknisi ca7b2fd
formatting
nicknisi bc369a3
fix build
nicknisi 61ef064
revert: restore original README philosophy and architecture
nicknisi ea71a0a
formatting
nicknisi aaa4a1d
clarify terminology and add explanatory comments
nicknisi 2084975
Update src/core/AuthKitCore.ts
nicknisi 881cbd0
update README
nicknisi ccfb0f6
default isTokenExpiring buffer to 10 rather than 60 (seconds)
nicknisi cfc802f
turn on erasableSyntaxOnly
nicknisi 5aeef49
pass resolved AuthKitConfig to Core and Operations instead of Configu…
nicknisi ef86d09
refactor: move lazy initialization from AuthService to factory
nicknisi 207dfad
pass resolved config to CookieSessionStorage
nicknisi 6b98bde
formatting
nicknisi dd17310
add some claims to top-level for convenience (match authkit-nextjs)
nicknisi 88acab1
refactor: simplify AuthOperations by delegating to core
nicknisi c6d7ae2
feat: support custom state passthrough in OAuth flow
nicknisi a9fb59d
docs: clarify encryption is framework's responsibility
nicknisi ae047f4
account for possibility exp is undefined
nicknisi 596f699
fix: remove proactive token refresh from server-side validation
nicknisi 2b4277b
fix: type errors in tests
nicknisi 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
Large diffs are not rendered by default.
Oops, something went wrong.
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
Large diffs are not rendered by default.
Oops, something went wrong.
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,265 @@ | ||
| import { AuthKitCore } from './AuthKitCore.js'; | ||
| import { SessionEncryptionError, TokenRefreshError } from './errors.js'; | ||
|
|
||
| const mockConfig = { | ||
| getValue: (key: string) => { | ||
| const values = { | ||
| cookiePassword: 'test-password-that-is-32-chars-long!!', | ||
| clientId: 'test-client-id', | ||
| }; | ||
| return values[key as keyof typeof values]; | ||
| }, | ||
| }; | ||
|
|
||
| const mockUser = { | ||
| id: 'user_123', | ||
| email: 'test@example.com', | ||
| object: 'user', | ||
| firstName: 'Test', | ||
| lastName: 'User', | ||
| emailVerified: true, | ||
| profilePictureUrl: null, | ||
| createdAt: '2023-01-01T00:00:00Z', | ||
| updatedAt: '2023-01-01T00:00:00Z', | ||
| lastSignInAt: '2023-01-01T00:00:00Z', | ||
| externalId: null, | ||
| metadata: {}, | ||
| } as const; | ||
|
|
||
| const mockClient = { | ||
| userManagement: { | ||
| getJwksUrl: () => 'https://api.workos.com/sso/jwks/test-client-id', | ||
| authenticateWithRefreshToken: async () => ({ | ||
| accessToken: 'new-access-token', | ||
| refreshToken: 'new-refresh-token', | ||
| user: mockUser, | ||
| impersonator: undefined, | ||
| }), | ||
| }, | ||
| }; | ||
|
|
||
| const mockEncryption = { | ||
| sealData: async () => 'encrypted-session-data', | ||
| unsealData: async () => ({ | ||
| accessToken: 'test-access-token', | ||
| refreshToken: 'test-refresh-token', | ||
| user: mockUser, | ||
| impersonator: undefined, | ||
| }), | ||
| }; | ||
|
|
||
| describe('AuthKitCore', () => { | ||
| let core: AuthKitCore; | ||
|
|
||
| beforeEach(() => { | ||
| core = new AuthKitCore( | ||
| mockConfig as any, | ||
| mockClient as any, | ||
| mockEncryption as any, | ||
| ); | ||
| }); | ||
|
|
||
| describe('constructor', () => { | ||
| it('creates instance with required dependencies', () => { | ||
| expect(core).toBeInstanceOf(AuthKitCore); | ||
| }); | ||
| }); | ||
|
|
||
| describe('parseTokenClaims()', () => { | ||
| it('parses valid JWT payload', () => { | ||
| const validJwt = | ||
| 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInNpZCI6InNlc3Npb25fMTIzIiwiZXhwIjoxNzM2Mzc2MDAwfQ.fake-signature'; | ||
|
|
||
| const result = core.parseTokenClaims(validJwt); | ||
|
|
||
| expect(result.sub).toBe('user_123'); | ||
| expect(result.sid).toBe('session_123'); | ||
| expect(result.exp).toBe(1736376000); | ||
| }); | ||
|
|
||
| it('throws error for invalid JWT', () => { | ||
| expect(() => core.parseTokenClaims('invalid-jwt')).toThrow( | ||
| 'Invalid token', | ||
| ); | ||
| }); | ||
|
|
||
| it('supports custom claims', () => { | ||
| const customJwt = | ||
| 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImN1c3RvbUZpZWxkIjoiY3VzdG9tLXZhbHVlIn0.fake-signature'; | ||
|
|
||
| const result = core.parseTokenClaims<{ customField: string }>(customJwt); | ||
|
|
||
| expect(result.customField).toBe('custom-value'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('isTokenExpiring()', () => { | ||
| it('returns true when token expires soon', () => { | ||
| const soonExpiry = Math.floor(Date.now() / 1000) + 30; | ||
| const expiringJwt = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${btoa(JSON.stringify({ exp: soonExpiry }))}.fake-signature`; | ||
|
|
||
| const result = core.isTokenExpiring(expiringJwt); | ||
|
|
||
| expect(result).toBe(true); | ||
| }); | ||
|
|
||
| it('returns false when token expires later', () => { | ||
| const laterExpiry = Math.floor(Date.now() / 1000) + 3600; | ||
| const validJwt = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${btoa(JSON.stringify({ exp: laterExpiry }))}.fake-signature`; | ||
|
|
||
| const result = core.isTokenExpiring(validJwt); | ||
|
|
||
| expect(result).toBe(false); | ||
| }); | ||
|
|
||
| it('uses custom buffer time', () => { | ||
| const expiry = Math.floor(Date.now() / 1000) + 150; | ||
| const jwt = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${btoa(JSON.stringify({ exp: expiry }))}.fake-signature`; | ||
|
|
||
| const result = core.isTokenExpiring(jwt, 180); | ||
|
|
||
| expect(result).toBe(true); | ||
| }); | ||
|
|
||
| it('returns false when token has no expiry', () => { | ||
| const noExpiryJwt = | ||
| 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyJ9.fake-signature'; | ||
|
|
||
| const result = core.isTokenExpiring(noExpiryJwt); | ||
|
|
||
| expect(result).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('verifyToken()', () => { | ||
| it('returns false for invalid tokens', async () => { | ||
| const result = await core.verifyToken('invalid-token'); | ||
|
|
||
| expect(result).toBe(false); | ||
| }); | ||
|
|
||
| it('returns false for malformed tokens', async () => { | ||
| const result = await core.verifyToken('not.a.jwt'); | ||
|
|
||
| expect(result).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('encryptSession()', () => { | ||
| it('encrypts session data', async () => { | ||
| const session = { | ||
| accessToken: 'test-token', | ||
| refreshToken: 'test-refresh', | ||
| user: mockUser, | ||
| impersonator: undefined, | ||
| }; | ||
|
|
||
| const result = await core.encryptSession(session); | ||
|
|
||
| expect(result).toBe('encrypted-session-data'); | ||
| }); | ||
|
|
||
| it('throws SessionEncryptionError on failure', async () => { | ||
| const failingEncryption = { | ||
| sealData: async () => { | ||
| throw new Error('Encryption failed'); | ||
| }, | ||
| unsealData: async () => ({}), | ||
| }; | ||
| const failingCore = new AuthKitCore( | ||
| mockConfig as any, | ||
| mockClient as any, | ||
| failingEncryption as any, | ||
| ); | ||
|
|
||
| await expect( | ||
| failingCore.encryptSession({ | ||
| accessToken: 'test', | ||
| refreshToken: 'test', | ||
| user: mockUser, | ||
| impersonator: undefined, | ||
| }), | ||
| ).rejects.toThrow(SessionEncryptionError); | ||
| }); | ||
| }); | ||
|
|
||
| describe('decryptSession()', () => { | ||
| it('decrypts session data', async () => { | ||
| const result = await core.decryptSession('encrypted-data'); | ||
|
|
||
| expect(result.accessToken).toBe('test-access-token'); | ||
| expect(result.user).toEqual(mockUser); | ||
| }); | ||
|
|
||
| it('throws SessionEncryptionError on failure', async () => { | ||
| const failingEncryption = { | ||
| sealData: async () => 'encrypted', | ||
| unsealData: async () => { | ||
| throw new Error('Decryption failed'); | ||
| }, | ||
| }; | ||
| const failingCore = new AuthKitCore( | ||
| mockConfig as any, | ||
| mockClient as any, | ||
| failingEncryption as any, | ||
| ); | ||
|
|
||
| await expect(failingCore.decryptSession('bad-data')).rejects.toThrow( | ||
| SessionEncryptionError, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('refreshTokens()', () => { | ||
| it('refreshes tokens via WorkOS', async () => { | ||
| const result = await core.refreshTokens('refresh-token'); | ||
|
|
||
| expect(result.accessToken).toBe('new-access-token'); | ||
| expect(result.refreshToken).toBe('new-refresh-token'); | ||
| expect(result.user).toEqual(mockUser); | ||
| }); | ||
|
|
||
| it('includes organizationId when provided', async () => { | ||
| const clientWithSpy = { | ||
| userManagement: { | ||
| getJwksUrl: () => 'https://api.workos.com/sso/jwks/test-client-id', | ||
| authenticateWithRefreshToken: async ({ organizationId }: any) => ({ | ||
| accessToken: organizationId ? 'org-token' : 'regular-token', | ||
| refreshToken: 'new-refresh-token', | ||
| user: mockUser, | ||
| impersonator: undefined, | ||
| }), | ||
| }, | ||
| }; | ||
| const testCore = new AuthKitCore( | ||
| mockConfig as any, | ||
| clientWithSpy as any, | ||
| mockEncryption as any, | ||
| ); | ||
|
|
||
| const result = await testCore.refreshTokens('refresh-token', 'org_123'); | ||
|
|
||
| expect(result.accessToken).toBe('org-token'); | ||
| }); | ||
|
|
||
| it('throws TokenRefreshError on failure', async () => { | ||
| const failingClient = { | ||
| userManagement: { | ||
| getJwksUrl: () => 'https://api.workos.com/sso/jwks/test-client-id', | ||
| authenticateWithRefreshToken: async () => { | ||
| throw new Error('Refresh failed'); | ||
| }, | ||
| }, | ||
| }; | ||
| const failingCore = new AuthKitCore( | ||
| mockConfig as any, | ||
| failingClient as any, | ||
| mockEncryption as any, | ||
| ); | ||
|
|
||
| await expect(failingCore.refreshTokens('bad-token')).rejects.toThrow( | ||
| TokenRefreshError, | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.