Skip to content

Commit 93bb9a4

Browse files
avmaximUzlopak
andauthored
docs: update Apple OAuth, recommend apple-signin-auth for new apple integrations (#189)
* Enhance docs & example of Apple OAuth2. Fixed merge conflicts with forked repo * Apply suggestions from code review --------- Co-authored-by: Uzlopak <[email protected]>
1 parent 943d43f commit 93bb9a4

File tree

2 files changed

+105
-27
lines changed

2 files changed

+105
-27
lines changed

examples/apple.js

Lines changed: 93 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,130 @@
11
'use strict'
22

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+
*/
911

1012
const fastify = require('fastify')({ logger: { level: 'trace' } })
11-
const appleSignin = require('apple-signin')
13+
const appleSignin = require('apple-signin-auth')
1214

1315
// const oauthPlugin = require('fastify-oauth2')
1416
const oauthPlugin = require('..')
1517

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()
1722

1823
fastify.register(oauthPlugin, {
1924
name: 'appleOAuth2',
2025
credentials: {
2126
client: {
2227
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
2629
},
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+
}
2839
},
2940
startRedirectPath: '/login/apple',
3041
callbackUri: 'http://localhost:3000/login/apple/callback'
3142
})
3243

3344
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) => {
3770
if (err) {
3871
reply.send(err)
3972
return
4073
}
4174

42-
appleSignin.verifyIdToken(
43-
result.id_token,
44-
CLIENT_ID
45-
)
75+
decryptToken(result.id_token)
4676
.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
4977
const userAppleId = payload.sub
50-
5178
reply.send(userAppleId)
5279
})
5380
.catch(err => {
5481
// Token is not verified
5582
reply.send(err)
5683
})
57-
}
58-
)
84+
})
5985
})
6086

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+
61130
fastify.listen({ port: 3000 })

test/index.test.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,10 @@ t.test('preset configuration generate-callback-uri-params', t => {
666666
id: 'my-client-id',
667667
secret: 'my-secret'
668668
},
669-
auth: fastifyOauth2.APPLE_CONFIGURATION
669+
auth: fastifyOauth2.APPLE_CONFIGURATION,
670+
options: {
671+
authorizationMethod: 'body'
672+
}
670673
},
671674
startRedirectPath: '/login/apple',
672675
callbackUri: '/callback',
@@ -702,7 +705,10 @@ t.test('preset configuration generate-callback-uri-params', t => {
702705
id: 'my-client-id',
703706
secret: 'my-secret'
704707
},
705-
auth: fastifyOauth2.APPLE_CONFIGURATION
708+
auth: fastifyOauth2.APPLE_CONFIGURATION,
709+
options: {
710+
authorizationMethod: 'body'
711+
}
706712
},
707713
startRedirectPath: '/login/apple',
708714
callbackUri: '/callback',
@@ -738,7 +744,10 @@ t.test('preset configuration generate-callback-uri-params', t => {
738744
id: 'my-client-id',
739745
secret: 'my-secret'
740746
},
741-
auth: fastifyOauth2.APPLE_CONFIGURATION
747+
auth: fastifyOauth2.APPLE_CONFIGURATION,
748+
options: {
749+
authorizationMethod: 'body'
750+
}
742751
},
743752
startRedirectPath: '/login/apple',
744753
callbackUri: '/callback',

0 commit comments

Comments
 (0)