Skip to content

Commit 2810f6b

Browse files
fix(providers): conform Apple (#12068)
1 parent be6d169 commit 2810f6b

File tree

5 files changed

+105
-40
lines changed

5 files changed

+105
-40
lines changed

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,7 @@ export async function handleOAuth(
209209
break
210210
}
211211
default:
212-
throw new TypeError(
213-
`Unrecognized provider conformation (${provider.id}).`
214-
)
212+
break
215213
}
216214
}
217215
const processedCodeResponse = await o.processAuthorizationCodeResponse(
@@ -230,6 +228,14 @@ export async function handleOAuth(
230228
const idTokenClaims = o.getValidatedIdTokenClaims(processedCodeResponse)!
231229
profile = idTokenClaims
232230

231+
// Apple sends some of the user information in a `user` parameter as a stringified JSON.
232+
// It also only does so the first time the user consents to share their information.
233+
if (provider[conformInternal] && provider.id === "apple") {
234+
try {
235+
profile.user = JSON.parse(params?.user)
236+
} catch {}
237+
}
238+
233239
if (provider.idToken === false) {
234240
const userinfoResponse = await o.userInfoRequest(
235241
as,

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ export async function getAuthorizationUrl(
6868

6969
const cookies: Cookie[] = []
7070

71+
if (
72+
// Otherwise "POST /redirect_uri" wouldn't include the cookies
73+
provider.authorization?.url.searchParams.get("response_mode") ===
74+
"form_post"
75+
) {
76+
options.cookies.state.options.sameSite = "none"
77+
options.cookies.state.options.secure = true
78+
options.cookies.nonce.options.sameSite = "none"
79+
options.cookies.nonce.options.secure = true
80+
}
81+
7182
const state = await checks.state.create(options, data)
7283
if (state) {
7384
authParams.set("state", state.value)

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,20 @@ export default function parseProviders(params: {
3939
})
4040

4141
if (provider.type === "oauth" || provider.type === "oidc") {
42-
merged.redirectProxyUrl ??= config.redirectProxyUrl
42+
merged.redirectProxyUrl ??=
43+
userOptions?.redirectProxyUrl ?? config.redirectProxyUrl
44+
4345
const normalized = normalizeOAuth(merged) as InternalProvider<
4446
"oauth" | "oidc"
4547
>
48+
// We currently don't support redirect proxies for response_mode=form_post
49+
if (
50+
normalized.authorization?.url.searchParams.get("response_mode") ===
51+
"form_post"
52+
) {
53+
delete normalized.redirectProxyUrl
54+
}
55+
4656
// @ts-expect-error Symbols don't get merged by the `merge` function
4757
// so we need to do it manually.
4858
normalized[customFetch] ??= userOptions?.[customFetch]

packages/core/src/providers/apple.ts

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* @module providers/apple
1212
*/
1313

14+
import { conformInternal, customFetch } from "../lib/symbols.js"
1415
import type { OAuthConfig, OAuthUserConfig } from "./index.js"
1516

1617
/** The returned user profile from Apple when using the profile callback. */
@@ -94,49 +95,59 @@ export interface AppleProfile extends Record<string, any> {
9495
transfer_sub: string
9596
at_hash: string
9697
auth_time: number
98+
user?: AppleNonConformUser
99+
}
100+
101+
/**
102+
* This is the shape of the `user` query parameter that Apple sends the first
103+
* time the user consents to the app.
104+
* @see https://developer.apple.com/documentation/sign_in_with_apple/request_an_authorization_to_the_sign_in_with_apple_server#4066168
105+
*/
106+
export interface AppleNonConformUser {
107+
name: {
108+
firstName: string
109+
lastName: string
110+
}
111+
email: string
97112
}
98113

99114
/**
100115
* ### Setup
101116
*
102117
* #### Callback URL
103118
* ```
104-
* https://example.com/api/auth/callback/apple
119+
* https://example.com/auth/callback/apple
105120
* ```
106121
*
107122
* #### Configuration
108123
* ```ts
109-
* import { Auth } from "@auth/core"
110124
* import Apple from "@auth/core/providers/apple"
111-
*
112-
* const request = new Request(origin)
113-
* const response = await Auth(request, {
114-
* providers: [
115-
* Apple({
116-
* clientId: APPLE_CLIENT_ID,
117-
* clientSecret: APPLE_CLIENT_SECRET,
118-
* }),
119-
* ],
120-
* })
125+
* ...
126+
* providers: [
127+
* Apple({
128+
* clientId: env.AUTH_APPLE_ID,
129+
* clientSecret: env.AUTH_APPLE_SECRET,
130+
* })
131+
* ]
132+
* ...
121133
* ```
122-
*
123-
*
124-
* Apple requires the client secret to be a JWT. You can generate one using the following script:
125-
* https://bal.so/apple-gen-secret
126-
*
127-
* Read more: [Creating the Client Secret
128-
](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048)
129-
*
134+
*
130135
* ### Resources
131-
*
136+
*
132137
* - Sign in with Apple [Overview](https://developer.apple.com/sign-in-with-apple/get-started/)
133138
* - Sign in with Apple [REST API](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api)
134139
* - [How to retrieve](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple#3383773) the user's information from Apple ID servers
135140
* - [Learn more about OAuth](https://authjs.dev/concepts/oauth)
136-
141+
* - [Creating the Client Secret](https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret)
142+
*
137143
* ### Notes
138-
*
139-
* The Apple provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/apple.ts). To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/configuring-oauth-providers).
144+
*
145+
* - Apple does not support localhost/http URLs. You can only use a live URL with HTTPS.
146+
* - Apple requires the client secret to be a JWT. We provide a CLI command `npx auth add apple`, to help you generate one.
147+
* This will prompt you for the necessary information and at the end it will add the `AUTH_APPLE_ID` and `AUTH_APPLE_SECRET` to your `.env` file.
148+
* - Apple provides minimal user information. It returns the user's email and name, but only the first time the user consents to the app.
149+
* - The Apple provider does not support setting up the same client for multiple deployments (like [preview deployments](https://authjs.dev/getting-started/deployment#securing-a-preview-deployment)).
150+
* - The Apple provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/apple.ts). To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/configuring-oauth-providers).
140151
*
141152
* ## Help
142153
*
@@ -146,26 +157,50 @@ export interface AppleProfile extends Record<string, any> {
146157
* the spec by the provider. You can open an issue, but if the problem is non-compliance with the spec,
147158
* we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions).
148159
*/
149-
export default function Apple<P extends AppleProfile>(
150-
options: OAuthUserConfig<P>
151-
): OAuthConfig<P> {
160+
export default function Apple(
161+
config: OAuthUserConfig<AppleProfile>
162+
): OAuthConfig<AppleProfile> {
152163
return {
153164
id: "apple",
154165
name: "Apple",
155166
type: "oidc",
156167
issuer: "https://appleid.apple.com",
157168
authorization: {
158-
params: { scope: "name email", response_mode: "form_post" },
169+
params: {
170+
scope: "name email", // https://developer.apple.com/documentation/sign_in_with_apple/clientconfigi/3230955-scope
171+
response_mode: "form_post",
172+
},
159173
},
160-
client: {
161-
token_endpoint_auth_method: "client_secret_post",
174+
// We need to parse the special `user` parameter the first time the user consents to the app.
175+
// It adds the `name` object to the `profile`, with `firstName` and `lastName` fields.
176+
[conformInternal]: true,
177+
profile(profile) {
178+
const name = profile.user
179+
? `${profile.user.name.firstName} ${profile.user.name.lastName}`
180+
: profile.email
181+
return {
182+
id: profile.sub,
183+
name: name,
184+
email: profile.email,
185+
image: null,
186+
}
162187
},
163-
style: {
164-
text: "#fff",
165-
bg: "#000",
188+
// Apple does not provide a userinfo endpoint.
189+
async [customFetch](...args) {
190+
const url = new URL(args[0] instanceof Request ? args[0].url : args[0])
191+
if (url.pathname.endsWith(".well-known/openid-configuration")) {
192+
const response = await fetch(...args)
193+
const json = await response.clone().json()
194+
return Response.json({
195+
...json,
196+
userinfo_endpoint: "https://appleid.apple.com/fake_endpoint",
197+
})
198+
}
199+
return fetch(...args)
166200
},
167-
// https://developer.apple.com/documentation/sign_in_with_apple/request_an_authorization_to_the_sign_in_with_apple_server
201+
client: { token_endpoint_auth_method: "client_secret_post" },
202+
style: { text: "#fff", bg: "#000" },
168203
checks: ["nonce", "state"],
169-
options,
204+
options: config,
170205
}
171206
}

packages/core/src/providers/oauth.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,10 @@ export type OAuthConfigInternal<Profile> = Omit<
270270
url: URL
271271
request?: TokenEndpointHandler["request"]
272272
clientPrivateKey?: CryptoKey | PrivateKey
273-
/** @internal */
273+
/**
274+
* @internal
275+
* @deprecated
276+
*/
274277
conform?: TokenEndpointHandler["conform"]
275278
}
276279
userinfo?: { url: URL; request?: UserinfoEndpointHandler["request"] }

0 commit comments

Comments
 (0)