Extract authentication business logic and remove deprecated code#6
Extract authentication business logic and remove deprecated code#6
Conversation
…ended
- Emphasize toolkit-first: Core + Operations are primitives
- AuthService is 'one way' not 'the way' to orchestrate
- Update exports: Primitives first, orchestration second (optional)
- Remove prescriptive language ('recommended', 'advanced')
- Acknowledge AuthService is used by TanStack Start as proof it works
- Preserve toolkit philosophy: frameworks choose their orchestration
- Remove 'toolkit' language from all documentation - Frame AuthService as public API, not 'one option' - Label Core + Operations as 'internal layers' (advanced use only) - Update package.json description to be accurate - Reorder exports: AuthService first, internals last - Acknowledge what this is: framework-agnostic library with adapter pattern - Stop pretending Core + Operations are composable primitives
03d1042 to
b802f54
Compare
There was a problem hiding this comment.
Pull Request Overview
This PR successfully extracts WorkOS authentication business logic into a framework-agnostic library, replacing the deprecated factory pattern with a clean service-based architecture. The refactor introduces improved type safety through discriminated unions and establishes clear separation of concerns between public API (AuthService) and internal implementation (AuthKitCore, AuthOperations).
Key changes:
- Introduced
AuthServiceas the main public API withcreateAuthService()factory - Removed deprecated
SessionManagerandcreateAuthKitFactory()(~500 lines) - Enhanced type safety with discriminated union
AuthResulttype (eliminates optional chaining) - Updated dependencies (jose 6.1.2, vitest 4.0.10, typescript 5.9.3)
Reviewed Changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/service/factory.ts | New factory function with lazy initialization for AuthService creation |
| src/service/AuthService.ts | New main public API coordinating core logic, operations, and storage |
| src/operations/AuthOperations.ts | New operations layer handling WorkOS API calls (signOut, refresh, URLs) |
| src/core/AuthKitCore.ts | New core business logic layer (JWT, encryption, refresh orchestration) |
| src/core/session/types.ts | Enhanced with discriminated union AuthResult type for better type safety |
| src/core/session/CookieSessionStorage.ts | Added secure flag inference and SameSite capitalization for Safari |
| src/index.ts | Reorganized exports with clear public API vs internal layers distinction |
| package.json | Updated dependencies and version to 0.2.0-beta.0 |
| README.md | Complete rewrite explaining architecture, storage adapter pattern, and usage |
| pnpm-lock.yaml | Dependency updates reflecting package.json changes |
| src/core/session/SessionManager.ts | Removed deprecated SessionManager class |
| src/core/createAuthKitFactory.ts | Removed deprecated factory pattern |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The 'toolkit' framing was fine. Overthinking the semantics added no value. Keep the good parts from original update: philosophy, architecture, examples.
cfaad70 to
61ef064
Compare
- replace vague "toolkit primitives" with "framework-agnostic authentication service" - update "toolkit API" to "public API" for clarity - add missing comment to empty catch block in token validation
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 19 changed files in this pull request and generated 5 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
5c753cc to
881cbd0
Compare
Greptile OverviewGreptile SummaryThis PR successfully extracts authentication business logic into a cleaner, more maintainable architecture with three distinct layers: Key improvements:
Breaking changes:
Security posture:
Confidence Score: 5/5
Important Files ChangedFile Analysis
Sequence DiagramsequenceDiagram
participant App as Framework App
participant AS as AuthService
participant Storage as SessionStorage
participant Core as AuthKitCore
participant Ops as AuthOperations
participant WorkOS as WorkOS API
Note over App,WorkOS: Authentication Flow (withAuth)
App->>AS: withAuth(request)
AS->>Storage: getSession(request)
Storage-->>AS: encryptedSession
AS->>Core: decryptSession(encryptedSession)
Core-->>AS: session (tokens + user)
AS->>Core: validateAndRefresh(session)
Core->>Core: verifyToken(accessToken)
Core->>Core: isTokenExpiring(accessToken)
alt Token valid and not expiring
Core-->>AS: valid session, no refresh
else Token invalid or expiring
Core->>WorkOS: authenticateWithRefreshToken()
WorkOS-->>Core: new tokens and user
Core->>Core: encryptSession(newSession)
Core-->>AS: refreshed session data
end
AS-->>App: AuthResult with optional refresh
Note over App,WorkOS: OAuth Callback Flow
App->>AS: handleCallback(request, response, options)
AS->>WorkOS: authenticateWithCode(authCode)
WorkOS-->>AS: tokens and user data
AS->>Core: encryptSession(session)
Core-->>AS: encryptedSession
AS->>Storage: saveSession(response, encryptedSession)
Storage-->>AS: updated response with cookie
AS-->>App: response with return path
Note over App,WorkOS: Sign Out Flow
App->>AS: signOut(sessionId, options)
AS->>Ops: signOut(sessionId, options)
Ops->>WorkOS: getLogoutUrl(sessionId)
WorkOS-->>Ops: logoutUrl
Ops->>Ops: buildClearCookieHeader()
Ops-->>AS: logout URL and clear cookie
AS-->>App: logout data for redirect
|
cmatheson
left a comment
There was a problem hiding this comment.
i'm still making my way through this, but posting my current set of comments now
src/core/AuthKitCore.ts
Outdated
| const { accessToken } = session; | ||
| const isValid = await this.verifyToken(accessToken); | ||
| const isExpiring = this.isTokenExpiring(accessToken); | ||
| if (isValid && !isExpiring) { |
There was a problem hiding this comment.
what is the point of this !isExpiring check?
There was a problem hiding this comment.
Proactive refresh. If the token expires within the buffer window, refresh it preemptively rather than waiting for it to actually expire.
There was a problem hiding this comment.
i think for server-side proactively refreshing is all downside? (you just refresh more often which is worse).
(we do want to proactively refresh for front-end consumers but i don't think this is used for that?).
There was a problem hiding this comment.
Yeah, you're right. I'll clean this up.
src/operations/AuthOperations.ts
Outdated
|
|
||
| // Build cookie clear header | ||
| const cookieName = this.config.getValue('cookieName'); | ||
| const clearCookieHeader = `${cookieName}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=lax`; |
There was a problem hiding this comment.
not a blocker but i think for a general-purpose library, operating on cookie headers seems like the wrong level of abstraction (like suppose a framework exists that has a proper session abstraction, we wouldn't want to be setting/clearing cookies directly).
There was a problem hiding this comment.
Good call. I refactored AuthOperations to just getLogoutUrl which will just return the logout url. AuthService#signOut will now do the orchestration.
const clearResult = await this.storage.clearSession();
return { logoutUrl, ...clearResult };That way, it leave it to the framework implementation to handle it however they need to.
src/operations/AuthOperations.ts
Outdated
| if (!orgId) { | ||
| // Extract org from current token (decodeJwt works even on expired tokens) | ||
| try { | ||
| const claims = this.core.parseTokenClaims<BaseTokenClaims>( |
There was a problem hiding this comment.
it seems like this is basically just 100% duplication of the core logic? why do we need this function?
There was a problem hiding this comment.
Good catch. Refactored refreshSession() to delegate to validateAndRefresh(session, { force: true, organizationId }).
src/operations/AuthOperations.ts
Outdated
| loginHint: options.loginHint, | ||
| prompt: options.prompt, | ||
| clientId: this.config.getValue('clientId'), | ||
| state: options.returnPathname |
There was a problem hiding this comment.
this is probably a weakness of the existing apis, but we're stopping our customers from other meaningful uses of state when we set it to returnPathname like this.
also this would be a good place for us to be preventing login-csrf (this sdk should be generating a nonce and passing it in here and storing in a cookie)
(login-csrf prevention could be a separate PR, but we should fix the issue of customers not being able to set state now).
There was a problem hiding this comment.
Done. I matched how we handle state in authkit-nextjs.
- Custom state passed as-is when no returnPathname
- Combined as {internal}.{userState} when both provided
- URL-safe base64 for internal state
- handleCallback returns the custom state to caller
There was a problem hiding this comment.
I'll handle the login-csrf issue in a separate PR.
src/service/AuthService.ts
Outdated
| /** | ||
| * Sign out operation - delegates to AuthOperations. | ||
| */ | ||
| async signOut(sessionId: string, options?: { returnTo?: string }) { |
There was a problem hiding this comment.
it's not clear to me that the different layers are all providing value. what's the point of all these functions that just call (an equally simple) implementation in the previous layer? could we just re-export? do we need AuthOperations?
There was a problem hiding this comment.
The layers provide composition flexibility. My idea is that
AuthKitCorehandles pure token logic (validation, refresh, encryption).AuthOperationshandles WorkOS API coordination (URLs, logout).AuthServiceadds storage orchestration for frameworks without session primitives.
src/service/AuthService.ts
Outdated
| // Authenticate with WorkOS using the OAuth code | ||
| const authResponse = await client.userManagement.authenticateWithCode({ | ||
| code: options.code, | ||
| clientId, |
There was a problem hiding this comment.
(for login-csrf prevention, this is where we would pull our nonce out of a cookie (or the session) and ensure that it matches the state param coming on the request.)
…rationProvider AuthService lazy getters resolve config when first accessed, preserving configure() flexibility while simplifying internal dependencies.
AuthService now takes resolved config, client, and encryption directly. Factory returns a lazy proxy that defers AuthService creation until first use. This keeps the same external API while simplifying internal dependencies.
- Add force/organizationId options to validateAndRefresh for org switching
- Simplify refreshSession to call validateAndRefresh({ force: true })
- Rename signOut to getLogoutUrl (just returns URL)
- Move session clearing orchestration to AuthService.signOut
- Add `state` option to GetAuthorizationUrlOptions - Use URL-safe base64 for internal state (returnPathname) - Combine as `internal.userState` when both provided - Pass custom state as-is when no returnPathname (matches authkit-nextjs) - Parse and return custom state in handleCallback result
Updated module docs to reflect that session encryption should be handled by framework adapters, with authkit-session providing a fallback. Exported sessionEncryption for frameworks to use.
Server-side requests are discrete - no benefit to refreshing valid tokens "just in case". Only refresh when token is actually invalid or force is true. Matches authkit-nextjs server behavior. isTokenExpiring() kept for client-side frameworks that want proactive refresh.
Summary
Extract authentication business logic into reusable classes and remove deprecated factory pattern. Provides clean API for building framework-specific WorkOS authentication packages.
Key Changes
New API:
createAuthService({ sessionStorageFactory })- Create auth instanceAuthService- Main API (withAuth, signOut, getSignInUrl, etc.)AuthResult- TypeScript-safe auth checksRemoved:
createAuthKitFactory()- DeprecatedSessionManager- DeprecatedInternal architecture:
AuthKitCore- JWT verification, session encryption, refresh logicAuthOperations- WorkOS API operationsBreaking Changes
1. Factory function renamed:
2. withAuth returns discriminated union:
3. AuthResult type:
Testing
Documentation