Dimitri James Tsiflitzis
CocoaHeads SKG #41
Sign in with Apple: Swift + Node.js
What is Sign in with Apple?
- A more "private" way to sign in to third-party apps and websites using your Apple ID.
- When you see a Sign in with Apple button on a participating app or website, it means you can set up an account using your Apple ID.
- No need to use a social media account, fill out forms or choose another new password.
What is Sign in with Apple?
Privacy and security
- On your first sign-in, apps and websites can ask only for your name and email address to set up an account for you.
- You can use Hide My Email to create a random email address that forwards to your Apple ID email.
- Apple claims Sign in with Apple won’t track or profile you, and only retains the information required to authenticate you. Third parties definately only receive a name and email.
- Sign in with Apple uses two-factor authentication. You can sign in and re-authenticate with Face ID or Touch ID anytime.
If you choose TO Hide YOUR Email
A random email address is created, and your personal email isn't shared with the app or website developer during the account setup.
<unique-string>@privaterelay.appleid.com
This unique relay address can only be used for communication from the specific app or website developer you created the account with. It can't be reused for other apps or services. (You set this up in the developer portal)
Part one: Client SIDE
swift
Getting Started
Add Capabilities:
Getting Started
Add a sign in button - SwiftUI:
/*
*/
import SwiftUI
import AuthenticationServices
class SignInWithApple: UIViewRepresentable {
// No customisation, so we return the view directly
func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
ASAuthorizationAppleIDButton()
}
// No state change, so do nothing
func updateUIView(_ uiView: ASAuthorizationAppleIDButton,
context: Context) {
}
}
Getting Started
Add a sign in button - UIKit:
/*
*/
import UIKit
func addAppleSignInButton() {
let button = ASAuthorizationAppleIDButton()
button.center = view.center
view.addSubview(button)
}
Getting Started
Button customizations:
- Frame size
- Corner radius
- Enum for various appearance styles:
.black, .whiteOutline, .white
ACTIONS
/*
*/
import UIKit
func addAppleSignInButton() {
let button = ASAuthorizationAppleIDButton()
button.center = view.center
view.addSubview(button)
button.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress),
for: .touchUpInside)
}
Add an action the usual way - addTarget:
ACTIONS
/*
*/
import UIKit
@objc func handleAuthorizationAppleIDButtonPress() {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
}
- ASAuthorizationController takes care of the UI and presentation logic.
- ASAuthorizationAppleIDRequest requests the info we require on our behalf.
ACTIONS
/*
*/
import UIKit
// MARK: - ASAuthorizationControllerPresentationContextProviding
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
guard
let window = self.window
else {
return nil
}
return window
}
We need to specify from where we will present the UI though.
Delegate methods
// MARK: - ASAuthorizationControllerDelegate
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
guard let appleIDToken = appleIDCredential.identityToken else {
print("Unable to fetch identity token")
return
}
guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
return
}
let userIdentifier = appleIDCredential.user
let fullName = appleIDCredential.fullName
let email = appleIDCredential.email
...
}
}
ASAuthorizationControllerDelegate has two methods that are called.
One for success:
Delegate methods
func authorizationController(controller: ASAuthorizationController,
didCompleteWithError error: Error) {
// Handle error.
guard let error = error as? ASAuthorizationError else {
return
}
switch error.code {
case .canceled: print("Canceled")
case .unknown: print("Unknown")
case .invalidResponse: print("Invalid Respone")
case .notHandled: print("Not handled")
case .failed: print("Failed")
@unknown default: print("Default")
}
}
One for failure:
error cases
No Apple ID
No 2FA Enabled
No iCloud Enabled
No Problem
UI Nuance
First time:
All subsequent times:
You get the works
"Apple id"
Identity Token
WHY?
Apple staff says:
SO...
- Save a full name and email since you can't retrieve it later, e.g., save it in the key chain
- Create an account in your system
- On fail, recover full name and email from the keychain and retry registration
Like this
Testing
Reset like it's the first time:
Settings >
Apple ID >
Password & Security >
Apple ID Logins/App Using Your Apple ID >
Your app name >
Stop using Apple ID
Interlude: JWT
BEFORE we HEAD ONLINE
One of the fields Apple gives us at "Sign in", is the identity token, which is a JWT (JSON Web Token).
The identity token lets us verify that that user string came from Apple, not a malicious third party. We verify by decoding and looking at the identity token's subject field and comparing that against the user string.
We can trust the identity token from Apple because the JWT is signed.
BEFORE we HEAD ONLINE
For instance, if can decode this token at jwt.io
BEFORE we HEAD ONLINE
Let's have a closer look
BEFORE we HEAD ONLINE
If we pass along both the identity token and the user string to our server, it can verify that the credentials are in fact valid and from Apple.
However, the token expires very soon after its generation.
This means that we can only verify their identity at that point in time.
Which is good enough in order to transition them into our own sytem.
Part TWO: SERVER SIDE
nodejs
the basics
Our Node.js back-end will be responsible for two things:
- Receive an identity token and send it to Apple to verify it
- Use that verification information to sign in, or create an account
How to verify the Identity token
So before we use the token (idenityToken), we need to make sure that it was signed by Apple's private key.
To do that, we need Apple's public key to verify the signature portion of the JWT.
You can get the public key from the following endpoint:
GET https://appleid.apple.com/auth/keys
How to verify the IDENTITY token
{
"keys": [
{
"kty": "RSA",
"kid": "AIDOPK1",
"use": "sig",
"alg": "RS256",
"n": "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4
Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfE
aY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_J
CpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-o
bP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0
XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7O
b-VMhug9eGyxAWVfu_1u6QJKePlE-w",
"e": "AQAB"
}
]
}
The response would look like this:
How to verify the IDENTITY token
The response would look like this:
This JSON is a JSON Web Key Set (JWKS)
{
"keys": [
{
"kty": "RSA",
"kid": "AIDOPK1",
"use": "sig",
"alg": "RS256",
"n": "lxrwmuYSAsTfn...u6QJKePlE-w",
"e": "AQAB"
}
]
}
It is a set of keys containing the public keys that use to verify JWT. In this case, it is a set of one key (Apple might add a new one in the future, so don't hard code it).
MINI Interlude: JSON Web Key Set Properties
Property name | Description |
---|---|
alg | The specific cryptographic algorithm used with the key. |
kty | The family of cryptographic algorithms used with the key. |
use | How the key was meant to be used; sig represents the signature. |
x5c | The x.509 certificate chain. The first entry in the array is the certificate to use for token verification; the other certificates can be used to verify this first certificate. |
n | The modulus for the RSA public key. |
e | The exponent for the RSA public key. |
kid | The unique identifier for the key. |
x5t | The thumbprint of the x.509 cert (SHA-1 thumbprint). |
Get the apple public key in node.js
import * as jwksClient from 'jwks-rsa';
export const APPLE_BASE_URL = 'https://appleid.apple.com';
export const JWKS_APPLE_URI = '/auth/keys';
export const getApplePublicKey = async (kid) => {
const client = jwksClient({
cache: true,
jwksUri: `${APPLE_BASE_URL}${JWKS_APPLE_URI}`,
});
const key: any = await new Promise((resolve, reject) => {
client.getSigningKey(kid, (error, result) => {
if (error) {
return reject(error);
}
return resolve(result);
});
});
return key.publicKey as jwksClient.CertSigningKey ||
key.rsaPublicKey as jwksClient.RsaSigningKey;
};
As a JSON web key set
VERIFY the identity TOKEN in node.js
import * as jwt from 'jsonwebtoken';
export const APPLE_BASE_URL = 'https://appleid.apple.com';
export interface VerifyAppleIdTokenParams {
idToken: string;
clientId: string;
nonce?: string;
}
export default async (params: VerifyAppleIdTokenParams) => {
const decoded = jwt.decode(params.idToken, { complete: true });
const { kid, alg } = decoded.header;
const applePublicKey = await getApplePublicKey(kid);
const jwtClaims = jwt.verify(params.idToken, applePublicKey, {
algorithms: [alg],
nonce: params.nonce,
});
if (!jwtClaims.iss ||
jwtClaims.iss !== APPLE_BASE_URL) {
throw new Error(`The iss does not match the Apple URL - iss: ${jwtClaims.iss} |
expected: ${APPLE_BASE_URL}`);
}
if (jwtClaims.aud !== params.clientId) {
throw new Error(`The aud parameter does not include this client - is: ${jwtClaims.aud} |
expected: ${params.clientId}`);
}
return jwtClaims;
};
tonights most important slide
It's not surprising at all to consider the user field as our "username" and our identity token as our "password" in our Swift code (ASAuthorizationAppleIDCredential).
In the identity token the expiry field (exp) is 10 minutes after its issue date (iat).
Not so fast.
Sorry
That means Apple is telling us not to consider the identity token valid for more than 10 minutes.
DIVORCE APPLE AUTHENTICATION FROM YOUR own AUTHENTICATION
Apple returns just enough information to verify users at the time of login and registration.
How we continue to trust them after that, without Apple's credentials is completely up to us.
A successful verification is trustworthy enough, for us to log our users in, or create them.
In my case it was JSON web tokens.
Login flow in node.js
const { siwa_token, email, apple_id, first_name, last_name } = req.body;
const user = await User.findOne({ apple_id: apple_id });
if (user) {
if (email && email !== user.email) {
try {
await User.update({apple_id: apple_id}, { email: email, apple_id: apple_id })
} catch(error) {
res.status(400).send({ error: `${error}`});
}
user.email = req.body.email;
}
tokenService.verify(req.body, (response) => {
if (response && response.message) {
res.status(401).send(response.message);
} else {
let webToken = genToken(user)
res.status(200).send({ id: user._id, token: webToken });
}
})
}
registration flow IN NODE.JS
else { //!user
tokenService.verify(req.body, (error) => {
if (error) {
res.status(401).send(error);
} else {
let newUser = User({
email: email,
apple_id: apple_id
})
let webToken = genToken(newUser)
newUser.save()
.then(() => {
res.status(200).json({
id: newUser._id,
token: webToken
});
})
.catch( error => {
res.status(400).json({ error: `${error}`});
})
}
})
}
in summary
Swift Client
Users are directed to providers, e.g., Apple, Google, Facebook, Twitter, Microsoft, Amazon (sigh). Users grant/deny permissions that the application requested. Providers direct users back to the application along with a token. The application then uses this token to sign in (or create an account).
NodeJS Backend
Use the token from the client to retrieve information from the provider, e.g., id, email. Use that information to sign in, or create an account if this is the first time.
Personal note: Implement JWT's on your backend so you can safely add new platforms in the future (web, android for instance)
ευχαριστουμε
🎈
Sign in with Apple
By tsif
Sign in with Apple
- 215