@logto/connector-apple 1.1.0 → 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 +22 -9
- package/docs/sign-in-with-apple-consent-page.png +0 -0
- package/lib/constant.d.ts +0 -1
- package/lib/index.js +48 -10
- package/lib/mock.d.ts +1 -1
- package/lib/types.d.ts +36 -0
- package/package.json +1 -1
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
|
-
- [
|
|
8
|
-
- [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- [
|
|
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
|
|
|
@@ -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
|
-
>
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+

|
|
80
|
+
|
|
81
|
+
See developer discussion [here](https://forums.developer.apple.com/forums/thread/132223).
|
|
69
82
|
|
|
70
83
|
## Test Apple connector
|
|
71
84
|
|
|
Binary file
|
package/lib/constant.d.ts
CHANGED
|
@@ -3,6 +3,5 @@ export declare const issuer = "https://appleid.apple.com";
|
|
|
3
3
|
export declare const authorizationEndpoint = "https://appleid.apple.com/auth/authorize";
|
|
4
4
|
export declare const accessTokenEndpoint = "https://appleid.apple.com/auth/token";
|
|
5
5
|
export declare const jwksUri = "https://appleid.apple.com/auth/keys";
|
|
6
|
-
export declare const scope = "";
|
|
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: '
|
|
1234
|
-
placeholder: '<
|
|
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: '
|
|
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
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
|
}>;
|