Skip to content

Commit fedff04

Browse files
feat(oauth): expose custom fetch (#11975)
* chore: upgrade packages * drop unused imports * feat: expose custom `fetch` option * properly forward user option, add corporate proxy guide * add to sidebar * expose `customFetch` symbol from `next-auth` * expose `customFetch` from other frameworks too
1 parent 917cb2c commit fedff04

File tree

15 files changed

+128
-41
lines changed

15 files changed

+128
-41
lines changed

docs/pages/guides/_meta.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ export default {
66
"extending-the-session": "Extending the Session",
77
"restricting-user-access": "Restricting users accessing to the app",
88
"role-based-access-control": "Role-Based Access Control",
9+
"corporate-proxy": "Supporting Corporate Proxies",
10+
"edge-compatibility": "Edge Compatibility",
911
"configuring-github": "Configuring GitHub for OAuth",
1012
"configuring-resend": "Configuring Resend for magic links",
1113
"configuring-oauth-providers": "Configuring OAuth providers",
1214
"configuring-http-email": "Configuring Custom HTTP Email Provider",
1315
"creating-a-database-adapter": "Creating a Database Adapter",
1416
"creating-a-framework-integration": "Creating a Framework Integration",
1517
"refresh-token-rotation": "Refresh Token Rotation",
16-
"edge-compatibility": "Edge Compatibility",
1718
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Code } from "@/components/Code"
2+
3+
# Supporting corporate proxies
4+
5+
Auth.js libraries like NextAuth.js use the `fetch` API to communicate with OAuth providers. If your organization uses a corporate proxy, you may need to configure the `fetch` API to use the proxy.
6+
7+
## Using a custom fetch function
8+
9+
You can provide a custom `fetch` function to NextAuth.js by passing it as an option to the provider. This allows you to configure the `fetch` function to use a proxy.
10+
11+
Here, we use the `undici` library to make requests through a proxy server. We create a custom `fetch` function that uses `undici` and the `ProxyAgent` library to make requests through the proxy server.
12+
13+
<Code>
14+
<Code.Next>
15+
16+
```tsx filename="auth.ts"
17+
import NextAuth, { customFetch } from "next-auth"
18+
import GitHub from "next-auth/providers/github"
19+
20+
const dispatcher = new ProxyAgent("my.proxy.server")
21+
function proxy(...args: Parameters<typeof fetch>): ReturnType<typeof fetch> {
22+
// @ts-expect-error `undici` has a `duplex` option
23+
return undici(args[0], { ...(args[1] ?? {}), dispatcher })
24+
}
25+
26+
export const { handlers, auth } = NextAuth({
27+
providers: [GitHub({ [customFetch]: proxy })],
28+
})
29+
```
30+
31+
</Code.Next>
32+
</Code>

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import type { CredentialInput, Provider } from "./providers/index.js"
6767
import { JWT, JWTOptions } from "./jwt.js"
6868
import { isAuthAction } from "./lib/utils/actions.js"
6969

70+
export { customFetch } from "./lib/utils/custom-fetch.js"
7071
export { skipCSRFCheck, raw, setEnvDefaults, createActionURL, isAuthAction }
7172

7273
export async function Auth(

packages/core/src/lib/actions/callback/oauth/callback.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
import { type OAuthConfigInternal } from "../../../../providers/index.js"
1818
import type { Cookie } from "../../../utils/cookie.js"
1919
import { isOIDCProvider } from "../../../utils/providers.js"
20+
import { fetchOpt } from "../../../utils/custom-fetch.js"
2021

2122
/**
2223
* Handles the following OAuth steps.
@@ -33,6 +34,7 @@ export async function handleOAuth(
3334
options: InternalOptions<"oauth" | "oidc">
3435
) {
3536
const { logger, provider } = options
37+
3638
let as: o.AuthorizationServer
3739

3840
const { token, userinfo } = provider
@@ -44,7 +46,10 @@ export async function handleOAuth(
4446
// We assume that issuer is always defined as this has been asserted earlier
4547

4648
const issuer = new URL(provider.issuer!)
47-
const discoveryResponse = await o.discoveryRequest(issuer)
49+
const discoveryResponse = await o.discoveryRequest(
50+
issuer,
51+
fetchOpt(provider)
52+
)
4853
const discoveredAs = await o.processDiscoveryResponse(
4954
issuer,
5055
discoveryResponse
@@ -114,7 +119,7 @@ export async function handleOAuth(
114119
) {
115120
args[1].body.delete("code_verifier")
116121
}
117-
return fetch(...args)
122+
return fetchOpt(provider)[o.customFetch](...args)
118123
},
119124
clientPrivateKey: provider.token?.clientPrivateKey,
120125
}
@@ -159,7 +164,8 @@ export async function handleOAuth(
159164
const userinfoResponse = await o.userInfoRequest(
160165
as,
161166
client,
162-
processedCodeResponse.access_token
167+
processedCodeResponse.access_token,
168+
fetchOpt(provider)
163169
)
164170

165171
profile = await o.processUserInfoResponse(
@@ -190,7 +196,8 @@ export async function handleOAuth(
190196
const userinfoResponse = await o.userInfoRequest(
191197
as,
192198
client,
193-
processedCodeResponse.access_token
199+
processedCodeResponse.access_token,
200+
fetchOpt(provider)
194201
)
195202
profile = await userinfoResponse.json()
196203
} else {

packages/core/src/lib/actions/signin/authorization-url.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as o from "oauth4webapi"
33

44
import type { InternalOptions, RequestInternal } from "../../../types.js"
55
import type { Cookie } from "../../utils/cookie.js"
6+
import { fetchOpt } from "../../utils/custom-fetch.js"
67

78
/**
89
* Generates an authorization/request token URL.
@@ -24,7 +25,10 @@ export async function getAuthorizationUrl(
2425
// We check this in assert.ts
2526

2627
const issuer = new URL(provider.issuer!)
27-
const discoveryResponse = await o.discoveryRequest(issuer)
28+
const discoveryResponse = await o.discoveryRequest(
29+
issuer,
30+
fetchOpt(options.provider)
31+
)
2832
const as = await o.processDiscoveryResponse(issuer, discoveryResponse)
2933

3034
if (!as.authorization_endpoint) {

packages/core/src/lib/init.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const defaultCallbacks: InternalOptions["callbacks"] = {
5252

5353
/** Initialize all internal options and cookies. */
5454
export async function init({
55-
authOptions,
55+
authOptions: config,
5656
providerId,
5757
action,
5858
url,
@@ -65,13 +65,8 @@ export async function init({
6565
options: InternalOptions
6666
cookies: cookie.Cookie[]
6767
}> {
68-
const logger = setLogger(authOptions)
69-
const { providers, provider } = parseProviders({
70-
providers: authOptions.providers,
71-
url,
72-
providerId,
73-
options: authOptions,
74-
})
68+
const logger = setLogger(config)
69+
const { providers, provider } = parseProviders({ url, providerId, config })
7570

7671
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle by default
7772

@@ -102,7 +97,7 @@ export async function init({
10297
buttonText: "",
10398
},
10499
// Custom options override defaults
105-
...authOptions,
100+
...config,
106101
// These computed settings can have values in userOptions but we override them
107102
// and are request-specific.
108103
url,
@@ -111,38 +106,38 @@ export async function init({
111106
provider,
112107
cookies: merge(
113108
cookie.defaultCookies(
114-
authOptions.useSecureCookies ?? url.protocol === "https:"
109+
config.useSecureCookies ?? url.protocol === "https:"
115110
),
116-
authOptions.cookies
111+
config.cookies
117112
),
118113
providers,
119114
// Session options
120115
session: {
121116
// If no adapter specified, force use of JSON Web Tokens (stateless)
122-
strategy: authOptions.adapter ? "database" : "jwt",
117+
strategy: config.adapter ? "database" : "jwt",
123118
maxAge,
124119
updateAge: 24 * 60 * 60,
125120
generateSessionToken: () => crypto.randomUUID(),
126-
...authOptions.session,
121+
...config.session,
127122
},
128123
// JWT options
129124
jwt: {
130-
secret: authOptions.secret!, // Asserted in assert.ts
131-
maxAge: authOptions.session?.maxAge ?? maxAge, // default to same as `session.maxAge`
125+
secret: config.secret!, // Asserted in assert.ts
126+
maxAge: config.session?.maxAge ?? maxAge, // default to same as `session.maxAge`
132127
encode: jwt.encode,
133128
decode: jwt.decode,
134-
...authOptions.jwt,
129+
...config.jwt,
135130
},
136131
// Event messages
137-
events: eventsErrorHandler(authOptions.events ?? {}, logger),
138-
adapter: adapterErrorHandler(authOptions.adapter, logger),
132+
events: eventsErrorHandler(config.events ?? {}, logger),
133+
adapter: adapterErrorHandler(config.adapter, logger),
139134
// Callback functions
140-
callbacks: { ...defaultCallbacks, ...authOptions.callbacks },
135+
callbacks: { ...defaultCallbacks, ...config.callbacks },
141136
logger,
142137
callbackUrl: url.origin,
143138
isOnRedirectProxy,
144139
experimental: {
145-
...authOptions.experimental,
140+
...config.experimental,
146141
},
147142
}
148143

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as o from "oauth4webapi"
2+
import type { InternalProvider } from "../../types.js"
3+
4+
/**
5+
* This advanced option allows you to override the default `fetch` function used by the provider
6+
* to make requests to the provider's OAuth endpoints.
7+
*
8+
* It can be used to support corporate proxies, custom fetch libraries, cache discovery endpoints,
9+
* add mocks for testing, logging, set custom headers/params for non-spec compliant providers, etc.
10+
*
11+
* @example
12+
* ```ts
13+
* import { Auth, customFetch } from "@auth/core"
14+
* import GitHub from "@auth/core/providers/github"
15+
*
16+
* const dispatcher = new ProxyAgent("my.proxy.server")
17+
* function proxy(...args: Parameters<typeof fetch>): ReturnType<typeof fetch> {
18+
* return undici(args[0], { ...(args[1] ?? {}), dispatcher })
19+
* }
20+
*
21+
* const response = await Auth(request, {
22+
* providers: [GitHub({ [customFetch]: proxy })]
23+
* })
24+
* ```
25+
*
26+
* @see https://undici.nodejs.org/#/docs/api/ProxyAgent?id=example-basic-proxy-request-with-local-agent-dispatcher
27+
* @see https://authjs.dev/guides/corporate-proxy
28+
*/
29+
export const customFetch = Symbol("custom-fetch")
30+
31+
/** @internal */
32+
export function fetchOpt(provider: InternalProvider<"oauth" | "oidc">) {
33+
return { [o.customFetch]: provider[customFetch] ?? fetch }
34+
}

packages/core/src/lib/utils/providers.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,27 @@ import type {
77
OAuthEndpointType,
88
OAuthUserConfig,
99
ProfileCallback,
10-
Provider,
1110
} from "../../providers/index.js"
1211
import type { InternalProvider, Profile } from "../../types.js"
13-
import type { AuthConfig } from "../../index.js"
12+
import { type AuthConfig } from "../../index.js"
13+
import { customFetch } from "../utils/custom-fetch.js"
1414

1515
/**
1616
* Adds `signinUrl` and `callbackUrl` to each provider
1717
* and deep merge user-defined options.
1818
*/
1919
export default function parseProviders(params: {
20-
providers: Provider[]
2120
url: URL
2221
providerId?: string
23-
options: AuthConfig
22+
config: AuthConfig
2423
}): {
2524
providers: InternalProvider[]
2625
provider?: InternalProvider
2726
} {
28-
const { providerId, options } = params
29-
const url = new URL(options.basePath ?? "/auth", params.url.origin)
27+
const { providerId, config } = params
28+
const url = new URL(config.basePath ?? "/auth", params.url.origin)
3029

31-
const providers = params.providers.map((p) => {
30+
const providers = config.providers.map((p) => {
3231
const provider = typeof p === "function" ? p() : p
3332
const { options: userOptions, ...defaults } = provider
3433

@@ -40,8 +39,14 @@ export default function parseProviders(params: {
4039
})
4140

4241
if (provider.type === "oauth" || provider.type === "oidc") {
43-
merged.redirectProxyUrl ??= options.redirectProxyUrl
44-
return normalizeOAuth(merged) as InternalProvider
42+
merged.redirectProxyUrl ??= config.redirectProxyUrl
43+
const normalized = normalizeOAuth(merged) as InternalProvider<
44+
"oauth" | "oidc"
45+
>
46+
// @ts-expect-error Symbols don't get merged by the `merge` function
47+
// so we need to do it manually.
48+
normalized[customFetch] ??= userOptions?.[customFetch]
49+
return normalized
4550
}
4651

4752
return merged as InternalProvider

packages/core/src/providers/oauth.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Client, PrivateKey } from "oauth4webapi"
22
import type { CommonProviderOptions } from "../providers/index.js"
33
import type { Awaitable, Profile, TokenSet, User } from "../types.js"
44
import type { AuthConfig } from "../index.js"
5+
import type { customFetch } from "../lib/utils/custom-fetch.js"
56

67
// TODO: fix types
78
type AuthorizationParameters = any
@@ -221,6 +222,8 @@ export interface OAuth2Config<Profile>
221222
*/
222223
allowDangerousEmailAccountLinking?: boolean
223224
redirectProxyUrl?: AuthConfig["redirectProxyUrl"]
225+
/** @see {customFetch} */
226+
[customFetch]?: typeof fetch
224227
/**
225228
* The options provided by the user.
226229
* We will perform a deep-merge of these values

packages/core/test/actions/callback.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2-
import * as jose from "jose"
3-
import * as o from "oauth4webapi"
42

53
import GitHub from "../../src/providers/github.js"
64
import Credentials from "../../src/providers/credentials.js"

0 commit comments

Comments
 (0)