Skip to content

Commit 0244513

Browse files
authored
chore: bump to oauth4webapi@3 (#11994)
1 parent 991e33e commit 0244513

File tree

6 files changed

+111
-82
lines changed

6 files changed

+111
-82
lines changed

packages/core/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"@types/cookie": "0.6.0",
7272
"cookie": "0.7.1",
7373
"jose": "^5.9.3",
74-
"oauth4webapi": "^2.17.0",
74+
"oauth4webapi": "^3.0.0",
7575
"preact": "10.11.3",
7676
"preact-render-to-string": "5.2.3"
7777
},

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

+93-60
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ import type { Cookie } from "../../../utils/cookie.js"
1919
import { isOIDCProvider } from "../../../utils/providers.js"
2020
import { fetchOpt } from "../../../utils/custom-fetch.js"
2121

22+
function formUrlEncode(token: string) {
23+
return encodeURIComponent(token).replace(/%20/g, "+")
24+
}
25+
26+
/**
27+
* Formats client_id and client_secret as an HTTP Basic Authentication header as per the OAuth 2.0
28+
* specified in RFC6749.
29+
*/
30+
function clientSecretBasic(clientId: string, clientSecret: string) {
31+
const username = formUrlEncode(clientId)
32+
const password = formUrlEncode(clientSecret)
33+
const credentials = btoa(`${username}:${password}`)
34+
return `Basic ${credentials}`
35+
}
36+
2237
/**
2338
* Handles the following OAuth steps.
2439
* https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
@@ -46,10 +61,11 @@ export async function handleOAuth(
4661
// We assume that issuer is always defined as this has been asserted earlier
4762

4863
const issuer = new URL(provider.issuer!)
49-
const discoveryResponse = await o.discoveryRequest(
50-
issuer,
51-
fetchOpt(provider)
52-
)
64+
// TODO: move away from allowing insecure HTTP requests
65+
const discoveryResponse = await o.discoveryRequest(issuer, {
66+
...fetchOpt(provider),
67+
[o.allowInsecureRequests]: true,
68+
})
5369
const discoveredAs = await o.processDiscoveryResponse(
5470
issuer,
5571
discoveryResponse
@@ -76,26 +92,63 @@ export async function handleOAuth(
7692

7793
const client: o.Client = {
7894
client_id: provider.clientId,
79-
client_secret: provider.clientSecret,
8095
...provider.client,
8196
}
8297

98+
let clientAuth: o.ClientAuth
99+
100+
switch (client.token_endpoint_auth_method) {
101+
// TODO: in the next breaking major version have undefined be `client_secret_post`
102+
case undefined:
103+
case "client_secret_basic":
104+
// TODO: in the next breaking major version use o.ClientSecretBasic() here
105+
clientAuth = (_as, _client, _body, headers) => {
106+
headers.set(
107+
"authorization",
108+
clientSecretBasic(provider.clientId, provider.clientSecret!)
109+
)
110+
}
111+
break
112+
case "client_secret_post":
113+
clientAuth = o.ClientSecretPost(provider.clientSecret!)
114+
break
115+
case "client_secret_jwt":
116+
clientAuth = o.ClientSecretJwt(provider.clientSecret!)
117+
break
118+
case "private_key_jwt":
119+
clientAuth = o.PrivateKeyJwt(provider.token!.clientPrivateKey!, {
120+
// TODO: review in the next breaking change
121+
[o.modifyAssertion](_header, payload) {
122+
payload.aud = [as.issuer, as.token_endpoint!]
123+
},
124+
})
125+
break
126+
default:
127+
throw new Error("unsupported client authentication method")
128+
}
129+
83130
const resCookies: Cookie[] = []
84131

85132
const state = await checks.state.use(cookies, resCookies, options)
86133

87-
const codeGrantParams = o.validateAuthResponse(
88-
as,
89-
client,
90-
new URLSearchParams(params),
91-
provider.checks.includes("state") ? state : o.skipStateCheck
92-
)
93-
94-
/** https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1 */
95-
if (o.isOAuth2Error(codeGrantParams)) {
96-
const cause = { providerId: provider.id, ...codeGrantParams }
97-
logger.debug("OAuthCallbackError", cause)
98-
throw new OAuthCallbackError("OAuth Provider returned an error", cause)
134+
let codeGrantParams: URLSearchParams
135+
try {
136+
codeGrantParams = o.validateAuthResponse(
137+
as,
138+
client,
139+
new URLSearchParams(params),
140+
provider.checks.includes("state") ? state : o.skipStateCheck
141+
)
142+
} catch (err) {
143+
if (err instanceof o.AuthorizationResponseError) {
144+
const cause = {
145+
providerId: provider.id,
146+
...Object.fromEntries(err.cause.entries()),
147+
}
148+
logger.debug("OAuthCallbackError", cause)
149+
throw new OAuthCallbackError("OAuth Provider returned an error", cause)
150+
}
151+
throw err
99152
}
100153

101154
const codeVerifier = await checks.pkce.use(cookies, resCookies, options)
@@ -108,20 +161,19 @@ export async function handleOAuth(
108161
let codeGrantResponse = await o.authorizationCodeGrantRequest(
109162
as,
110163
client,
164+
clientAuth,
111165
codeGrantParams,
112166
redirect_uri,
113-
codeVerifier ?? "auth", // TODO: review fallback code verifier,
167+
codeVerifier ?? "decoy",
114168
{
169+
// TODO: move away from allowing insecure HTTP requests
170+
[o.allowInsecureRequests]: true,
115171
[o.customFetch]: (...args) => {
116-
if (
117-
!provider.checks.includes("pkce") &&
118-
args[1]?.body instanceof URLSearchParams
119-
) {
172+
if (!provider.checks.includes("pkce")) {
120173
args[1].body.delete("code_verifier")
121174
}
122175
return fetchOpt(provider)[o.customFetch](...args)
123176
},
124-
clientPrivateKey: provider.token?.clientPrivateKey,
125177
}
126178
)
127179

@@ -131,41 +183,35 @@ export async function handleOAuth(
131183
codeGrantResponse
132184
}
133185

134-
let challenges: o.WWWAuthenticateChallenge[] | undefined
135-
if ((challenges = o.parseWwwAuthenticateChallenges(codeGrantResponse))) {
136-
for (const challenge of challenges) {
137-
console.log("challenge", challenge)
138-
}
139-
throw new Error("TODO: Handle www-authenticate challenges as needed")
140-
}
141-
142186
let profile: Profile = {}
143-
let tokens: TokenSet & Pick<Account, "expires_at">
144187

145-
if (isOIDCProvider(provider)) {
146-
const nonce = await checks.nonce.use(cookies, resCookies, options)
147-
const processedCodeResponse =
148-
await o.processAuthorizationCodeOpenIDResponse(
149-
as,
150-
client,
151-
codeGrantResponse,
152-
nonce ?? o.expectNoNonce
153-
)
154-
155-
if (o.isOAuth2Error(processedCodeResponse)) {
156-
console.log("error", processedCodeResponse)
157-
throw new Error("TODO: Handle OIDC response body error")
188+
const isOidc = isOIDCProvider(provider)
189+
const processedCodeResponse = await o.processAuthorizationCodeResponse(
190+
as,
191+
client,
192+
codeGrantResponse,
193+
{
194+
expectedNonce: await checks.nonce.use(cookies, resCookies, options),
195+
requireIdToken: isOidc,
158196
}
197+
)
198+
199+
const tokens: TokenSet & Pick<Account, "expires_at"> = processedCodeResponse
159200

160-
const idTokenClaims = o.getValidatedIdTokenClaims(processedCodeResponse)
201+
if (isOidc) {
202+
const idTokenClaims = o.getValidatedIdTokenClaims(processedCodeResponse)!
161203
profile = idTokenClaims
162204

163205
if (provider.idToken === false) {
164206
const userinfoResponse = await o.userInfoRequest(
165207
as,
166208
client,
167209
processedCodeResponse.access_token,
168-
fetchOpt(provider)
210+
{
211+
...fetchOpt(provider),
212+
// TODO: move away from allowing insecure HTTP requests
213+
[o.allowInsecureRequests]: true,
214+
}
169215
)
170216

171217
profile = await o.processUserInfoResponse(
@@ -175,20 +221,7 @@ export async function handleOAuth(
175221
userinfoResponse
176222
)
177223
}
178-
tokens = processedCodeResponse
179224
} else {
180-
const processedCodeResponse =
181-
await o.processAuthorizationCodeOAuth2Response(
182-
as,
183-
client,
184-
codeGrantResponse
185-
)
186-
tokens = processedCodeResponse
187-
if (o.isOAuth2Error(processedCodeResponse)) {
188-
console.log("error", processedCodeResponse)
189-
throw new Error("TODO: Handle OAuth 2.0 response body error")
190-
}
191-
192225
if (userinfo?.request) {
193226
const _profile = await userinfo.request({ tokens, provider })
194227
if (_profile instanceof Object) profile = _profile

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

+5-4
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ export async function getAuthorizationUrl(
2525
// We check this in assert.ts
2626

2727
const issuer = new URL(provider.issuer!)
28-
const discoveryResponse = await o.discoveryRequest(
29-
issuer,
30-
fetchOpt(options.provider)
31-
)
28+
// TODO: move away from allowing insecure HTTP requests
29+
const discoveryResponse = await o.discoveryRequest(issuer, {
30+
...fetchOpt(options.provider),
31+
[o.allowInsecureRequests]: true,
32+
})
3233
const as = await o.processDiscoveryResponse(issuer, discoveryResponse)
3334

3435
if (!as.authorization_endpoint) {

packages/core/src/providers/oauth.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export interface OAuth2Config<Profile>
205205
* Pass overrides to the underlying OAuth library.
206206
* See [`oauth4webapi` client](https://github.com/panva/oauth4webapi/blob/main/docs/interfaces/Client.md) for details.
207207
*/
208-
client?: Partial<Client>
208+
client?: Partial<Client & { token_endpoint_auth_method: string }>
209209
style?: OAuthProviderButtonStyles
210210
/**
211211
* Normally, when you sign in with an OAuth provider and another account

packages/core/src/types.ts

+6-11
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,7 @@
5252
*/
5353

5454
import type { CookieSerializeOptions } from "cookie"
55-
import type {
56-
OAuth2TokenEndpointResponse,
57-
OpenIDTokenEndpointResponse,
58-
} from "oauth4webapi"
55+
import type { TokenEndpointResponse } from "oauth4webapi"
5956
import type { Adapter } from "./adapters.js"
6057
import { AuthConfig } from "./index.js"
6158
import type { JWTOptions } from "./jwt.js"
@@ -102,9 +99,7 @@ export interface Theme {
10299
* Some of them are available with different casing,
103100
* but they refer to the same value.
104101
*/
105-
export type TokenSet = Partial<
106-
OAuth2TokenEndpointResponse | OpenIDTokenEndpointResponse
107-
> & {
102+
export type TokenSet = Partial<TokenEndpointResponse> & {
108103
/**
109104
* Date of when the `access_token` expires in seconds.
110105
* This value is calculated from the `expires_in` value.
@@ -118,7 +113,7 @@ export type TokenSet = Partial<
118113
* Usually contains information about the provider being used
119114
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
120115
*/
121-
export interface Account extends Partial<OpenIDTokenEndpointResponse> {
116+
export interface Account extends Partial<TokenEndpointResponse> {
122117
/** Provider's id for this account. E.g. "google". See the full list at https://authjs.dev/reference/core/providers */
123118
provider: string
124119
/**
@@ -137,11 +132,11 @@ export interface Account extends Partial<OpenIDTokenEndpointResponse> {
137132
*/
138133
userId?: string
139134
/**
140-
* Calculated value based on {@link OAuth2TokenEndpointResponse.expires_in}.
135+
* Calculated value based on {@link TokenEndpointResponse.expires_in}.
141136
*
142-
* It is the absolute timestamp (in seconds) when the {@link OAuth2TokenEndpointResponse.access_token} expires.
137+
* It is the absolute timestamp (in seconds) when the {@link TokenEndpointResponse.access_token} expires.
143138
*
144-
* This value can be used for implementing token rotation together with {@link OAuth2TokenEndpointResponse.refresh_token}.
139+
* This value can be used for implementing token rotation together with {@link TokenEndpointResponse.refresh_token}.
145140
*
146141
* @see https://authjs.dev/guides/refresh-token-rotation#database-strategy
147142
* @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1

pnpm-lock.yaml

+5-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)