@logto/connector-apple 1.0.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,12 +4,13 @@ The official Logto connector for Apple social sign-in.
4
4
 
5
5
  **Table of contents**
6
6
 
7
- - [Apple connector](#apple-connector)
8
- - [Get started](#get-started)
9
- - [Enable Sign in with Apple for your app](#enable-sign-in-with-apple-for-your-app)
10
- - [Create an identifier](#create-an-identifier)
11
- - [Enable Sign in with Apple for your identifier](#enable-sign-in-with-apple-for-your-identifier)
12
- - [Test Apple connector](#test-apple-connector)
7
+ - [Get started](#get-started)
8
+ - [Enable Sign in with Apple for your app](#enable-sign-in-with-apple-for-your-app)
9
+ - [Create an identifier](#create-an-identifier)
10
+ - [Enable Sign in with Apple for your identifier](#enable-sign-in-with-apple-for-your-identifier)
11
+ - [Configure scope](#configure-scope)
12
+ - [Pitfalls of configuring scope](#pitfalls-of-configuring-scope)
13
+ - [Test Apple connector](#test-apple-connector)
13
14
 
14
15
  ## Get started
15
16
 
@@ -30,7 +31,7 @@ You need to enroll [Apple Developer Program](https://developer.apple.com/program
30
31
 
31
32
  You can do it via Xcode -> Project settings -> Signing & Capabilities, or visit [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list/bundleId).
32
33
 
33
- ![Enable Sign in with Apple](/packages/connector-apple/docs/enable-sign-in-with-apple-in-xcode.png)
34
+ ![Enable Sign in with Apple](/packages/connectors/connector-apple/docs/enable-sign-in-with-apple-in-xcode.png)
34
35
 
35
36
  See the "Enable an App ID" section in [Apple official docs](https://developer.apple.com/documentation/sign_in_with_apple/configuring_your_environment_for_sign_in_with_apple) for more info.
36
37
 
@@ -45,13 +46,13 @@ See the "Enable an App ID" section in [Apple official docs](https://developer.ap
45
46
 
46
47
  Click the identifier you just created. Check "Sign in with Apple" on the details page and click "Configure".
47
48
 
48
- ![Enable Sign in with Apple](/packages/connector-apple/docs/enable-sign-in-with-apple.png)
49
+ ![Enable Sign in with Apple](/packages/connectors/connector-apple/docs/enable-sign-in-with-apple.png)
49
50
 
50
51
  In the opening modal, select the App ID you just enabled Sign in with Apple.
51
52
 
52
53
  Enter the domain of your Logto instance without protocol and port, e.g., `your.logto.domain`; then enter the "Return URL" (i.e., Redirect URI), which is the Logto URL with `/callback/${connector_id}`, e.g., `https://your.logto.domain/callback/apple-universal`. You can get the randomly generated `connector_id` after creating Apple connector in Admin Console.
53
54
 
54
- ![domain-and-url](/packages/connector-apple/docs/domain-and-url.png)
55
+ ![domain-and-url](/packages/connectors/connector-apple/docs/domain-and-url.png)
55
56
 
56
57
  Click "Next" then "Done" to close the modal. Click "Continue" on the top-right corner, then click "Save" to save your configuration.
57
58
 
@@ -61,11 +62,23 @@ Click "Next" then "Done" to close the modal. Click "Continue" on the top-right c
61
62
  >
62
63
  > If you want to test locally, you need to edit `/etc/hosts` file to map localhost to a custom domain and set up a local HTTPS environment. [mkcert](https://github.com/FiloSottile/mkcert) can help you for setting up local HTTPS.
63
64
 
65
+ ## Configure scope
66
+
67
+ To get user's email from Apple, you need to configure the scope to include `email`. For both email and name, you can use `name email` as the scope. See [Apple official docs](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113) for more info.
68
+
64
69
  > ℹ️ **Note**
65
70
  >
66
- > This connector doesn't support customizing `scope` (e.g., name, email) yet since Apple requires `form_post` response mode when `scope` is not empty, which is incompatible with the current connector design.
67
- >
68
- > We'll figure out this later.
71
+ > The user may choose to hide their email address from your app. In this case, you will not be able to retrieve the real email address. An email address like `random@privaterelay.appleid.com` will be returned instead.
72
+
73
+ ### Pitfalls of configuring scope
74
+
75
+ If you have configured your app to request users' email addresses after they have already signed in with Apple, you will not be able to retrieve the email addresses for those existing users, even if they sign in again using Apple ID. To address this, you need to instruct your users to visit the [Apple ID account management page](https://appleid.apple.com/account/manage) and remove your application from the "Sign in with Apple" section. This can be done by selecting "Stop using Apple Sign In" on your app's detail page.
76
+
77
+ For instance, if your app requests both the users' email and name (`email name` scope), the consent page that new users see during their first sign-in should look similar to this:
78
+
79
+ ![Sign in with Apple consent page](/packages/connectors/connector-apple/docs/sign-in-with-apple-consent-page.png)
80
+
81
+ See developer discussion [here](https://forums.developer.apple.com/forums/thread/132223).
69
82
 
70
83
  ## Test Apple connector
71
84
 
package/lib/constant.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import type { ConnectorMetadata } from '@logto/connector-kit';
2
2
  export declare const issuer = "https://appleid.apple.com";
3
- export declare const authorizationEndpoint: string;
4
- export declare const accessTokenEndpoint: string;
5
- export declare const jwksUri: string;
6
- export declare const scope = "";
3
+ export declare const authorizationEndpoint = "https://appleid.apple.com/auth/authorize";
4
+ export declare const accessTokenEndpoint = "https://appleid.apple.com/auth/token";
5
+ export declare const jwksUri = "https://appleid.apple.com/auth/keys";
7
6
  export declare const defaultMetadata: ConnectorMetadata;
8
7
  export declare const defaultTimeout = 5000;
package/lib/index.js CHANGED
@@ -2,12 +2,12 @@ import { ConnectorPlatform, ConnectorConfigFormItemType, ConnectorType, validate
2
2
  import * as crypto from 'node:crypto';
3
3
  import crypto__default, { KeyObject, createPrivateKey, createPublicKey, constants, createSecretKey } from 'node:crypto';
4
4
  import { z } from 'zod';
5
- import * as http from 'node:http';
6
- import * as https from 'node:https';
7
- import { once } from 'node:events';
8
5
  import { Buffer as Buffer$1 } from 'node:buffer';
9
6
  import * as util from 'node:util';
10
7
  import { promisify } from 'node:util';
8
+ import * as http from 'node:http';
9
+ import * as https from 'node:https';
10
+ import { once } from 'node:events';
11
11
 
12
12
  // https://github.com/facebook/jest/issues/7547
13
13
  const assert = (value, error) => {
@@ -1204,8 +1204,6 @@ function createRemoteJWKSet(url, options) {
1204
1204
  const issuer = 'https://appleid.apple.com';
1205
1205
  const authorizationEndpoint = `${issuer}/auth/authorize`;
1206
1206
  const jwksUri = `${issuer}/auth/keys`;
1207
- // Note: only support fixed scope for v1.
1208
- const scope = ''; // Note: `openid` is required when adding more scope(s)
1209
1207
  const defaultMetadata = {
1210
1208
  id: 'apple-universal',
1211
1209
  target: 'apple',
@@ -1230,18 +1228,50 @@ const defaultMetadata = {
1230
1228
  key: 'clientId',
1231
1229
  type: ConnectorConfigFormItemType.Text,
1232
1230
  required: true,
1233
- label: 'Client ID',
1234
- placeholder: '<client-id>',
1231
+ label: 'Identifier',
1232
+ placeholder: '<your-registered-identifier>',
1233
+ },
1234
+ {
1235
+ key: 'scope',
1236
+ type: ConnectorConfigFormItemType.Text,
1237
+ required: false,
1238
+ label: 'Scope',
1239
+ placeholder: 'email name',
1235
1240
  },
1236
1241
  ],
1237
1242
  };
1238
1243
 
1239
1244
  const appleConfigGuard = z.object({
1240
1245
  clientId: z.string(),
1246
+ scope: z.string().optional(),
1247
+ });
1248
+ const stringToJson = () => z.string().transform((value, ctx) => {
1249
+ try {
1250
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1251
+ return JSON.parse(value);
1252
+ }
1253
+ catch {
1254
+ ctx.addIssue({ code: 'custom', message: 'Invalid JSON' });
1255
+ return z.NEVER;
1256
+ }
1241
1257
  });
1242
1258
  // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3331292
1259
+ // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113
1243
1260
  const dataGuard = z.object({
1244
1261
  id_token: z.string(),
1262
+ user: stringToJson()
1263
+ .pipe(z
1264
+ .object({
1265
+ name: z
1266
+ .object({
1267
+ firstName: z.string(),
1268
+ lastName: z.string(),
1269
+ })
1270
+ .partial(),
1271
+ email: z.string(),
1272
+ })
1273
+ .partial())
1274
+ .optional(),
1245
1275
  });
1246
1276
 
1247
1277
  const generateNonce = () => generateStandardId();
@@ -1252,12 +1282,12 @@ const getAuthorizationUri = (getConfig) => async ({ state, redirectUri }, setSes
1252
1282
  const queryParameters = new URLSearchParams({
1253
1283
  client_id: config.clientId,
1254
1284
  redirect_uri: redirectUri,
1255
- scope,
1285
+ scope: config.scope ?? '',
1256
1286
  state,
1257
1287
  nonce,
1258
1288
  // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113
1259
1289
  response_type: 'code id_token',
1260
- response_mode: 'fragment',
1290
+ response_mode: 'form_post',
1261
1291
  });
1262
1292
  assert(setSession, new ConnectorError(ConnectorErrorCodes.NotImplemented, {
1263
1293
  message: "'setSession' is not implemented.",
@@ -1266,7 +1296,7 @@ const getAuthorizationUri = (getConfig) => async ({ state, redirectUri }, setSes
1266
1296
  return `${authorizationEndpoint}?${queryParameters.toString()}`;
1267
1297
  };
1268
1298
  const getUserInfo = (getConfig) => async (data, getSession) => {
1269
- const { id_token: idToken } = await authorizationCallbackHandler(data);
1299
+ const { id_token: idToken, user } = await authorizationCallbackHandler(data);
1270
1300
  if (!idToken) {
1271
1301
  throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
1272
1302
  }
@@ -1296,6 +1326,14 @@ const getUserInfo = (getConfig) => async (data, getSession) => {
1296
1326
  }
1297
1327
  return {
1298
1328
  id: payload.sub,
1329
+ // The `user` object is only available at the first sign-in. Didn't find this in Apple's
1330
+ // docs but it seems to be the case. Fallback to the `email` field in the ID token just in
1331
+ // case.
1332
+ // See desperate developer discussion here:
1333
+ // https://forums.developer.apple.com/forums/thread/132223
1334
+ email: user?.email ??
1335
+ (payload.email && payload.email_verified === true ? String(payload.email) : undefined),
1336
+ name: [user?.name?.firstName, user?.name?.lastName].filter(Boolean).join(' ') || undefined,
1299
1337
  };
1300
1338
  }
1301
1339
  catch {
package/lib/mock.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export declare const mockedConfig: {
2
2
  clientId: string;
3
- clientSecret: string;
3
+ scope: string;
4
4
  };
package/lib/types.d.ts CHANGED
@@ -1,16 +1,52 @@
1
1
  import { z } from 'zod';
2
2
  export declare const appleConfigGuard: z.ZodObject<{
3
3
  clientId: z.ZodString;
4
+ scope: z.ZodOptional<z.ZodString>;
4
5
  }, "strip", z.ZodTypeAny, {
5
6
  clientId: string;
7
+ scope?: string | undefined;
6
8
  }, {
7
9
  clientId: string;
10
+ scope?: string | undefined;
8
11
  }>;
9
12
  export type AppleConfig = z.infer<typeof appleConfigGuard>;
10
13
  export declare const dataGuard: z.ZodObject<{
11
14
  id_token: z.ZodString;
15
+ user: z.ZodOptional<z.ZodPipeline<z.ZodEffects<z.ZodString, z.ZodType<JSON, z.ZodTypeDef, JSON>, string>, z.ZodObject<{
16
+ name: z.ZodOptional<z.ZodObject<{
17
+ firstName: z.ZodOptional<z.ZodString>;
18
+ lastName: z.ZodOptional<z.ZodString>;
19
+ }, "strip", z.ZodTypeAny, {
20
+ firstName?: string | undefined;
21
+ lastName?: string | undefined;
22
+ }, {
23
+ firstName?: string | undefined;
24
+ lastName?: string | undefined;
25
+ }>>;
26
+ email: z.ZodOptional<z.ZodString>;
27
+ }, "strip", z.ZodTypeAny, {
28
+ name?: {
29
+ firstName?: string | undefined;
30
+ lastName?: string | undefined;
31
+ } | undefined;
32
+ email?: string | undefined;
33
+ }, {
34
+ name?: {
35
+ firstName?: string | undefined;
36
+ lastName?: string | undefined;
37
+ } | undefined;
38
+ email?: string | undefined;
39
+ }>>>;
12
40
  }, "strip", z.ZodTypeAny, {
13
41
  id_token: string;
42
+ user?: {
43
+ name?: {
44
+ firstName?: string | undefined;
45
+ lastName?: string | undefined;
46
+ } | undefined;
47
+ email?: string | undefined;
48
+ } | undefined;
14
49
  }, {
15
50
  id_token: string;
51
+ user?: string | undefined;
16
52
  }>;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@logto/connector-apple",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "Apple web connector implementation.",
5
5
  "dependencies": {
6
- "@logto/connector-kit": "^2.0.0",
7
- "@logto/shared": "^3.0.0",
6
+ "@logto/connector-kit": "^2.1.0",
7
+ "@logto/shared": "^3.1.0",
8
8
  "jose": "^5.0.0"
9
9
  },
10
10
  "main": "./lib/index.js",
@@ -19,7 +19,7 @@
19
19
  "logo-dark.svg"
20
20
  ],
21
21
  "engines": {
22
- "node": "^18.12.0"
22
+ "node": "^20.9.0"
23
23
  },
24
24
  "eslintConfig": {
25
25
  "extends": "@silverhand",