Skip to content

feat(core): support multiple authorization types in refreshToken config#224

Open
halvaradop wants to merge 1 commit into
masterfrom
feat/add-auth-types
Open

feat(core): support multiple authorization types in refreshToken config#224
halvaradop wants to merge 1 commit into
masterfrom
feat/add-auth-types

Conversation

@halvaradop

@halvaradop halvaradop commented Jul 10, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added support for configurable OAuth refresh-token authorization using Basic or credentials-based authentication.
    • Improved handling of refresh-token expiry information and token scopes.
    • OAuth callbacks now provide provider-specific access tokens for subsequent use.
    • Added support for Atlassian OAuth documentation references.
  • Bug Fixes

    • Improved token refresh behavior when expiry metadata is missing.
    • Standardized refreshed token data and authorization headers.
    • Restricted the configured app OAuth provider to GitHub.

@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
auth Skipped Skipped Jul 10, 2026 11:09pm

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

OAuth token schemas and payload transformation now support refresh-token expiry and configurable authorization. Refresh requests handle basic or credentials authentication, callbacks create provider-scoped JWT cookies, and related tests and configuration are updated.

Changes

OAuth token flow

Layer / File(s) Summary
Token contracts and payload normalization
packages/core/src/@types/oauth.ts, packages/core/src/schemas.ts, packages/core/src/shared/assert.ts, packages/core/src/shared/utils.ts
OAuth refresh configurations support authorization modes and token responses support refresh expiry values. Token payload transformation and type guards are added.
Configurable refresh requests
packages/core/src/api/getProviderTokens.ts, packages/core/test/actions/tokens/tokens.test.ts, packages/core/test/api/getAccessToken.test.ts, packages/core/test/api/getProviderTokens.test.ts
Refresh requests conditionally use Basic authorization or client credentials, merge configured headers, preserve initial response headers, and validate both modes.
Callback provider token cookie
packages/core/src/actions/callback/callback.ts, apps/nextjs/app-router/src/lib/auth.ts
OAuth callbacks create JWT provider tokens and set provider-scoped cookies; the application OAuth allowlist is restricted to GitHub.
Provider documentation and package formatting
packages/core/src/oauth/atlassian.ts, packages/react/package.json
An Atlassian OAuth reference is added, and the React package file ending is reformatted without metadata changes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OAuthProvider
  participant OAuthCallback
  participant jwtManager
  participant Browser
  OAuthProvider->>OAuthCallback: accessToken response
  OAuthCallback->>jwtManager: createToken(token payload)
  jwtManager-->>OAuthCallback: providerToken
  OAuthCallback->>Browser: Set-Cookie provider-scoped access token
Loading

Possibly related PRs

Suggested labels: feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding multiple authorization types to refreshToken config.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-auth-types

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/core/test/actions/tokens/tokens.test.ts (1)

196-248: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mock returns OAuthTokenPayload (camelCase) instead of OAuthAccessTokenResponseType (snake_case).

Both the basic-auth and credentials-auth tests use json: async () => oauthTokens as the fetch mock. oauthTokens has camelCase properties (accessToken, expiresAt, refreshToken), but refreshProviderToken reads snake_case fields (data.access_token, data.expires_in, data.refresh_token). Since those are all undefined, every value falls back to payload.*. The tests pass but never exercise the response-parsing logic — a field-name regression would go undetected.

♻️ Proposed fix: use a snake_case mock response
 const mockFetch = vi.fn().mockResolvedValueOnce({
     ok: true,
-    json: async () => oauthTokens,
+    json: async () => ({
+        access_token: "new-access-token",
+        expires_in: 7200,
+        refresh_token: "new-refresh-token",
+        refresh_token_expires_in: 86400,
+        token_type: "Bearer",
+        scope: "scope1 scope2",
+        id_token: undefined,
+    }),
 })

Then update the response assertion to expect the transformed values rather than ...oauthTokens with fallbacks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/actions/tokens/tokens.test.ts` around lines 196 - 248,
Update the fetch mocks in the credentials-auth and basic-auth refresh tests to
return an OAuthAccessTokenResponseType-shaped object with snake_case fields
(access_token, expires_in, refresh_token) instead of oauthTokens. Adjust the
response assertions to verify the values transformed from those snake_case
fields, ensuring refreshProviderToken exercises response parsing rather than
payload fallbacks.
packages/core/src/shared/utils.ts (1)

239-253: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Capture Date.now() once for timestamp consistency.

Date.now() is called three separate times (lines 243, 246, 251). While the difference is typically negligible, expiresAt, refreshTokenExpiresAt, and issuedAt could have slightly different base timestamps under edge conditions. Extract a single const now = Math.floor(Date.now() / 1000) and reuse it.

♻️ Proposed refactor
 export const transformToTokenPayload = (tokens: OAuthAccessTokenResponseType & { id_token?: string }) => {
+    const now = Math.floor(Date.now() / 1000)
     return {
         accessToken: tokens.access_token,
-        expiresAt: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : undefined,
+        expiresAt: tokens.expires_in ? now + tokens.expires_in : undefined,
         refreshToken: tokens.refresh_token,
         refreshTokenExpiresAt: tokens.refresh_token_expires_in
-            ? Math.floor(Date.now() / 1000) + tokens.refresh_token_expires_in
+            ? now + tokens.refresh_token_expires_in
             : undefined,
         idToken: tokens.id_token,
         tokenType: tokens.token_type,
         scopes: isString(tokens.scope) ? [tokens.scope] : Array.isArray(tokens.scope) ? tokens.scope : [],
-        issuedAt: Math.floor(Date.now() / 1000),
+        issuedAt: now,
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/shared/utils.ts` around lines 239 - 253, Update
transformToTokenPayload to compute a single const now = Math.floor(Date.now() /
1000) before constructing the payload, then reuse now for expiresAt,
refreshTokenExpiresAt, and issuedAt instead of calling Date.now() repeatedly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/actions/callback/callback.ts`:
- Around line 135-136: Default the token type to "Bearer" when constructing the
provider token in the callback flow: ensure the payload returned by
transformToTokenPayload has tokenType set to "Bearer" whenever the OAuth
response omits token_type, before passing it to context.jwtManager.createToken.
This keeps it valid for OAuthTokenPayloadSchema and parseOAuthTokens.

In `@packages/core/src/api/getProviderTokens.ts`:
- Around line 29-39: In the provider token request logic, avoid calling
createBasicAuthHeader unconditionally before determining the authorization mode.
Compute isCredentialsAuth first, then create the Basic auth header only when it
is needed, while preserving the existing credentials-auth behavior and request
headers.
- Line 23: Fix the refresh-token validation in getProviderTokens by explicitly
rejecting object-shaped refreshToken values that lack a url property; do not
rely on isRefreshTokenObject for this check because it already requires url.
Preserve support for string URLs and valid refresh-token objects, and ensure
invalid objects are rejected before fetchAsync receives them.

---

Nitpick comments:
In `@packages/core/src/shared/utils.ts`:
- Around line 239-253: Update transformToTokenPayload to compute a single const
now = Math.floor(Date.now() / 1000) before constructing the payload, then reuse
now for expiresAt, refreshTokenExpiresAt, and issuedAt instead of calling
Date.now() repeatedly.

In `@packages/core/test/actions/tokens/tokens.test.ts`:
- Around line 196-248: Update the fetch mocks in the credentials-auth and
basic-auth refresh tests to return an OAuthAccessTokenResponseType-shaped object
with snake_case fields (access_token, expires_in, refresh_token) instead of
oauthTokens. Adjust the response assertions to verify the values transformed
from those snake_case fields, ensuring refreshProviderToken exercises response
parsing rather than payload fallbacks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 22101bc6-c2dd-44d8-a912-094decb7f5e3

📥 Commits

Reviewing files that changed from the base of the PR and between 9df9d7e and 1315438.

📒 Files selected for processing (12)
  • apps/nextjs/app-router/src/lib/auth.ts
  • packages/core/src/@types/oauth.ts
  • packages/core/src/actions/callback/callback.ts
  • packages/core/src/api/getProviderTokens.ts
  • packages/core/src/oauth/atlassian.ts
  • packages/core/src/schemas.ts
  • packages/core/src/shared/assert.ts
  • packages/core/src/shared/utils.ts
  • packages/core/test/actions/tokens/tokens.test.ts
  • packages/core/test/api/getAccessToken.test.ts
  • packages/core/test/api/getProviderTokens.test.ts
  • packages/react/package.json

Comment on lines +135 to +136
const tokenPayload = transformToTokenPayload(accessToken)
const providerToken = await context.jwtManager.createToken(tokenPayload)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the createToken implementation to check if it validates the payload
rg -n 'createToken' packages/core/src --type=ts -C3

Repository: aura-stack-ts/auth

Length of output: 4578


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== transformToTokenPayload / OAuthTokenPayloadSchema ==\n'
rg -n 'transformToTokenPayload|OAuthTokenPayloadSchema|tokenType|token_type' packages/core/src --type=ts -C4

printf '\n== JWTManager createToken implementation / typings ==\n'
rg -n 'createToken\\s*:' packages/core/src --type=ts -C4
rg -n 'encodeJWT|signJWS|encryptJWE|jose-manager' packages/core/src --type=ts -C4

printf '\n== parseOAuthTokens / getProviderTokens path ==\n'
rg -n 'parseOAuthTokens|getProviderTokens|refreshProviderToken' packages/core/src --type=ts -C4

Repository: aura-stack-ts/auth

Length of output: 14222


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== jose-manager ==\n'
sed -n '1,120p' packages/core/src/session/jose-manager.ts

printf '\n== validator registry oauth section ==\n'
sed -n '160,215p' packages/core/src/validator/registry.ts

printf '\n== getProviderTokens ==\n'
sed -n '1,180p' packages/core/src/api/getProviderTokens.ts

printf '\n== parseOAuthTokens search ==\n'
rg -n 'parseOAuthTokens' packages/core/src -C4

Repository: aura-stack-ts/auth

Length of output: 11583


Default tokenType when building provider tokens.
transformToTokenPayload can emit tokenType: undefined because OAuthAccessTokenResponse.token_type is optional, but OAuthTokenPayloadSchema.tokenType is required. That means the cookie can be written successfully and then fail when parseOAuthTokens reads it back. Defaulting to "Bearer" here avoids the mismatch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/actions/callback/callback.ts` around lines 135 - 136,
Default the token type to "Bearer" when constructing the provider token in the
callback flow: ensure the payload returned by transformToTokenPayload has
tokenType set to "Bearer" whenever the OAuth response omits token_type, before
passing it to context.jwtManager.createToken. This keeps it valid for
OAuthTokenPayloadSchema and parseOAuthTokens.

provider: RuntimeOAuthProvider
): Promise<OAuthTokenPayload> => {
if (!provider.refreshToken || (typeof provider.refreshToken === "object" && !("url" in provider.refreshToken))) {
if (!provider.refreshToken || (isRefreshTokenObject(provider.refreshToken) && !("url" in provider.refreshToken))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Dead validation: object without url is never caught.

isRefreshTokenObject already checks "url" in value, so isRefreshTokenObject(provider.refreshToken) && !("url" in provider.refreshToken) is always false. An object-shaped refreshToken missing url passes validation, then line 29 falls to the else branch and passes the raw object as the URL to fetchAsync, causing a runtime failure.

🐛 Proposed fix
-    if (!provider.refreshToken || (isRefreshTokenObject(provider.refreshToken) && !("url" in provider.refreshToken))) {
+    if (!provider.refreshToken || (typeof provider.refreshToken === "object" && !isRefreshTokenObject(provider.refreshToken)) {
         throw new AuraAuthError({ code: "OAUTH_INVALID_REFRESH_TOKEN_CONFIG" })
     }

This throws when refreshToken is an object that lacks url, while still allowing string URLs and valid objects with url.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/api/getProviderTokens.ts` at line 23, Fix the refresh-token
validation in getProviderTokens by explicitly rejecting object-shaped
refreshToken values that lack a url property; do not rely on
isRefreshTokenObject for this check because it already requires url. Preserve
support for string URLs and valid refresh-token objects, and ensure invalid
objects are rejected before fetchAsync receives them.

Comment on lines +29 to +39
const url = isRefreshTokenObject(provider.refreshToken) ? provider.refreshToken.url : provider.refreshToken
const basicAuth = createBasicAuthHeader(provider.clientId!, provider.clientSecret!)

const isCredentialsAuth = isRefreshTokenObject(provider.refreshToken)
? provider.refreshToken.authorization?.type === "credentials"
: false
const response = await fetchAsync(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
...(isCredentialsAuth ? {} : { Authorization: basicAuth }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

createBasicAuthHeader called unconditionally — can throw in credentials-auth mode.

createBasicAuthHeader is invoked on line 30 before isCredentialsAuth is computed. When authorization.type === "credentials", the Basic header is never used (line 39 spreads {}), yet the call still executes. If clientId or clientSecret is missing, it throws AUTH_BASIC_CREDENTIALS_INVALID — a misleading error for a code path that sends credentials in the body, not as a Basic header.

🐛 Proposed fix: defer basic-auth creation until needed
     const url = isRefreshTokenObject(provider.refreshToken) ? provider.refreshToken.url : provider.refreshToken
-    const basicAuth = createBasicAuthHeader(provider.clientId!, provider.clientSecret!)

     const isCredentialsAuth = isRefreshTokenObject(provider.refreshToken)
         ? provider.refreshToken.authorization?.type === "credentials"
         : false
     const response = await fetchAsync(url, {
         method: "POST",
         headers: {
             "Content-Type": "application/x-www-form-urlencoded",
-            ...(isCredentialsAuth ? {} : { Authorization: basicAuth }),
+            ...(isCredentialsAuth ? {} : { Authorization: createBasicAuthHeader(provider.clientId!, provider.clientSecret!) }),
             ...(typeof provider.refreshToken === "object" && provider.refreshToken.headers ? provider.refreshToken.headers : {}),
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const url = isRefreshTokenObject(provider.refreshToken) ? provider.refreshToken.url : provider.refreshToken
const basicAuth = createBasicAuthHeader(provider.clientId!, provider.clientSecret!)
const isCredentialsAuth = isRefreshTokenObject(provider.refreshToken)
? provider.refreshToken.authorization?.type === "credentials"
: false
const response = await fetchAsync(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
...(isCredentialsAuth ? {} : { Authorization: basicAuth }),
const url = isRefreshTokenObject(provider.refreshToken) ? provider.refreshToken.url : provider.refreshToken
const isCredentialsAuth = isRefreshTokenObject(provider.refreshToken)
? provider.refreshToken.authorization?.type === "credentials"
: false
const response = await fetchAsync(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
...(isCredentialsAuth ? {} : { Authorization: createBasicAuthHeader(provider.clientId!, provider.clientSecret!) }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/api/getProviderTokens.ts` around lines 29 - 39, In the
provider token request logic, avoid calling createBasicAuthHeader
unconditionally before determining the authorization mode. Compute
isCredentialsAuth first, then create the Basic auth header only when it is
needed, while preserving the existing credentials-auth behavior and request
headers.

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