|
1 | 1 | 'use strict'
|
2 | 2 |
|
3 |
| -// This example assumes the use of the npm package apple-signin in your code. |
4 |
| -// This library is not included with fastify-oauth2. If you wish to implement |
5 |
| -// the verification part of Apple's Sign In REST API yourself, |
6 |
| -// look at https://github.com/Techofficer/node-apple-signin to see how they did |
7 |
| -// it, or look at https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api |
8 |
| -// for more details on how to do it from scratch. |
| 3 | +/** |
| 4 | + * This example assumes the use of the npm package `apple-signin-auth` in your code. |
| 5 | + * This library is not included with fastify-oauth2. If you wish to implement |
| 6 | + * the verification part of Apple's Sign In REST API yourself, |
| 7 | + * look at {@link https://github.com/a-tokyo/apple-signin-auth} to see how they did |
| 8 | + * it, or look at {@link https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api} |
| 9 | + * for more details on how to do it from scratch. |
| 10 | + */ |
9 | 11 |
|
10 | 12 | const fastify = require('fastify')({ logger: { level: 'trace' } })
|
11 |
| -const appleSignin = require('apple-signin') |
| 13 | +const appleSignin = require('apple-signin-auth') |
12 | 14 |
|
13 | 15 | // const oauthPlugin = require('fastify-oauth2')
|
14 | 16 | const oauthPlugin = require('..')
|
15 | 17 |
|
16 |
| -const CLIENT_ID = '<CLIENT_ID>' |
| 18 | +// All fields below must come from environment variables |
| 19 | +const [CLIENT_ID, TEAM_ID, PRIVATE_KEY, KEY_ID] = ['<CLIENT_ID>', '<TEAM_ID>', '<PRIVATE_KEY>', '<KEY_ID>'] |
| 20 | +// In Apple OAuth2 the CLIENT_SECRET is not static and must be generated |
| 21 | +const CLIENT_SECRET = generateClientSecret() |
17 | 22 |
|
18 | 23 | fastify.register(oauthPlugin, {
|
19 | 24 | name: 'appleOAuth2',
|
20 | 25 | credentials: {
|
21 | 26 | client: {
|
22 | 27 | id: CLIENT_ID,
|
23 |
| - // See https://github.com/Techofficer/node-apple-signin/blob/master/source/index.js |
24 |
| - // for how to create the secret. |
25 |
| - secret: '<CLIENT_SECRET>' |
| 28 | + secret: CLIENT_SECRET |
26 | 29 | },
|
27 |
| - auth: oauthPlugin.APPLE_CONFIGURATION |
| 30 | + auth: oauthPlugin.APPLE_CONFIGURATION, |
| 31 | + options: { |
| 32 | + /** |
| 33 | + * Based on offical Apple OAuth2 docs, an HTTP POST request is sent to the redirectURI for the `form_post` value. |
| 34 | + * And the result of the authorization is stored in the body as application/x-www-form-urlencoded content type. |
| 35 | + * See {@link https://developer.apple.com/documentation/sign_in_with_apple/request_an_authorization_to_the_sign_in_with_apple_server} |
| 36 | + */ |
| 37 | + authorizationMethod: 'body' |
| 38 | + } |
28 | 39 | },
|
29 | 40 | startRedirectPath: '/login/apple',
|
30 | 41 | callbackUri: 'http://localhost:3000/login/apple/callback'
|
31 | 42 | })
|
32 | 43 |
|
33 | 44 | fastify.get('/login/apple/callback', function (request, reply) {
|
34 |
| - this.appleOAuth2.getAccessTokenFromAuthorizationCodeFlow( |
35 |
| - request, |
36 |
| - (err, result) => { |
| 45 | + /** |
| 46 | + * NOTE: Apple returns the "user" object only the 1st time the user authorizes the app. |
| 47 | + * For more information, visit https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple |
| 48 | + */ |
| 49 | + const { code, state, error, user } = request.body |
| 50 | + |
| 51 | + if (user) { |
| 52 | + // Make sure to validate and persist it. All subsequent authorization requests will not contain the user object |
| 53 | + } |
| 54 | + |
| 55 | + if (!state) { |
| 56 | + // If the endpoint was not redirected from social oauth flow |
| 57 | + throw new Error('Illegal invoking of endpoint.') |
| 58 | + } |
| 59 | + |
| 60 | + if (error === Error.CancelledAuth) { |
| 61 | + // If a user cancelled authorization process, redirect him back to the app |
| 62 | + const webClientUrl = '<WEB_CLIENT_URL>' |
| 63 | + reply.status(303).redirect(webClientUrl) |
| 64 | + } |
| 65 | + |
| 66 | + const authCodeFlow = { ...request, query: { code, state } } |
| 67 | + |
| 68 | + this.appleOAuth2 |
| 69 | + .getAccessTokenFromAuthorizationCodeFlow(authCodeFlow, (err, result) => { |
37 | 70 | if (err) {
|
38 | 71 | reply.send(err)
|
39 | 72 | return
|
40 | 73 | }
|
41 | 74 |
|
42 |
| - appleSignin.verifyIdToken( |
43 |
| - result.id_token, |
44 |
| - CLIENT_ID |
45 |
| - ) |
| 75 | + decryptToken(result.id_token) |
46 | 76 | .then(payload => {
|
47 |
| - // Find all the available fields (like email) in |
48 |
| - // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple |
49 | 77 | const userAppleId = payload.sub
|
50 |
| - |
51 | 78 | reply.send(userAppleId)
|
52 | 79 | })
|
53 | 80 | .catch(err => {
|
54 | 81 | // Token is not verified
|
55 | 82 | reply.send(err)
|
56 | 83 | })
|
57 |
| - } |
58 |
| - ) |
| 84 | + }) |
59 | 85 | })
|
60 | 86 |
|
| 87 | +/** |
| 88 | + * Decrypts Token from Apple and returns decrypted user's info |
| 89 | + * |
| 90 | + * @param { string } token Info received from Apple's Authorization flow on Token request |
| 91 | + * @returns { object } Decrypted user's info |
| 92 | + */ |
| 93 | +function decryptToken (token) { |
| 94 | + /** |
| 95 | + * NOTE: Data format returned by Apple |
| 96 | + * |
| 97 | + * { |
| 98 | + * email: 'user_email@abc.com', |
| 99 | + * iss: 'https://appleid.apple.com' |
| 100 | + * sub: '10*****************27' // User ID, |
| 101 | + * email_verified: 'true', |
| 102 | + * is_private_email: 'false', |
| 103 | + * ... |
| 104 | + * } |
| 105 | + * |
| 106 | + * PS: All fields can be found here - {@link https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple} |
| 107 | + */ |
| 108 | + |
| 109 | + return appleSignin.verifyIdToken(token, CLIENT_ID) |
| 110 | +} |
| 111 | + |
| 112 | +/** |
| 113 | + * Generates Apple's OAuth2 secret key based on expiration date, Client ID, Team ID, Private key and Key ID. |
| 114 | + * See more {@link https://github.com/a-tokyo/apple-signin-auth} for implementation details. |
| 115 | + * |
| 116 | + * @returns { string } Apple Secret Key |
| 117 | + */ |
| 118 | +function generateClientSecret () { |
| 119 | + const expiresIn = 180 // in days (6 months) - custom time set based on requirements |
| 120 | + |
| 121 | + return appleSignin.getClientSecret({ |
| 122 | + clientID: CLIENT_ID, |
| 123 | + teamID: TEAM_ID, |
| 124 | + privateKey: PRIVATE_KEY, |
| 125 | + keyIdentifier: KEY_ID, |
| 126 | + expAfter: expiresIn * 24 * 3600 // in seconds |
| 127 | + }) |
| 128 | +} |
| 129 | + |
61 | 130 | fastify.listen({ port: 3000 })
|
0 commit comments