diff --git a/.gitignore b/.gitignore index e4c2c57..2642f8a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/* package-lock.json dist coverage +.vscode/* diff --git a/package.json b/package.json index 2a3374c..a23767a 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,7 @@ "url": "https://github.com/stefan-prokop-cz/verify-apple-id-token/issues" }, "dependencies": { - "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^3.0.0" + "jose": "^4.14.4" }, "devDependencies": { "@types/jest": "29.5.2", @@ -58,4 +57,4 @@ "ts-jest": "26.5.6", "typescript": "4.9.5" } -} +} \ No newline at end of file diff --git a/src/lib/verifyAppleIdToken.ts b/src/lib/verifyAppleIdToken.ts index 535cdf7..6c7abd1 100644 --- a/src/lib/verifyAppleIdToken.ts +++ b/src/lib/verifyAppleIdToken.ts @@ -1,45 +1,34 @@ -import * as jwt from "jsonwebtoken"; -import * as jwksClient from "jwks-rsa"; -import { VerifyAppleIdTokenParams, VerifyAppleIdTokenResponse } from "./types"; +import { createRemoteJWKSet, decodeProtectedHeader, jwtVerify } from "jose"; +import { VerifyAppleIdTokenParams } from "./types"; export const APPLE_BASE_URL = "https://appleid.apple.com"; export const JWKS_APPLE_URI = "/auth/keys"; -export const getApplePublicKey = async (kid: string) => { - const client = jwksClient({ - cache: true, - jwksUri: `${APPLE_BASE_URL}${JWKS_APPLE_URI}`, +export const getApplePublicKey = async (kid: string, alg: string) => { + const JWKS = createRemoteJWKSet(new URL(`${APPLE_BASE_URL}${JWKS_APPLE_URI}`)); + const key = await JWKS({ + alg, + kid, }); - const key = await new Promise((resolve, reject) => { - client.getSigningKey(kid, (error, result) => { - if (error) { - return reject(error); - } - return resolve(result); - }); - }); - return key.getPublicKey(); + return key; }; export const verifyToken = async (params: VerifyAppleIdTokenParams) => { - const decoded = jwt.decode(params.idToken, { complete: true }); - const { kid, alg } = decoded.header; + const { alg, kid } = decodeProtectedHeader(params.idToken); - const applePublicKey = await getApplePublicKey(kid); - const jwtClaims = jwt.verify(params.idToken, applePublicKey, { - algorithms: [alg as jwt.Algorithm], - nonce: params.nonce, - }) as VerifyAppleIdTokenResponse; + const applePublicKey = await getApplePublicKey(kid, alg); + + const { payload: jwtClaims } = await jwtVerify(params.idToken, applePublicKey); + + if (jwtClaims?.nonce !== params.nonce) { + throw new Error(`The nonce parameter does not match - nonce: ${jwtClaims.nonce} | expected: ${params.nonce}`); + } if (jwtClaims?.iss !== APPLE_BASE_URL) { - throw new Error( - `The iss does not match the Apple URL - iss: ${jwtClaims.iss} | expected: ${APPLE_BASE_URL}` - ); + throw new Error(`The iss does not match the Apple URL - iss: ${jwtClaims.iss} | expected: ${APPLE_BASE_URL}`); } - const isFounded = [] - .concat(jwtClaims.aud) - .some((aud) => [].concat(params.clientId).includes(aud)); + const isFounded = [].concat(jwtClaims.aud).some((aud) => [].concat(params.clientId).includes(aud)); if (isFounded) { ["email_verified", "is_private_email"].forEach((field) => { @@ -51,7 +40,5 @@ export const verifyToken = async (params: VerifyAppleIdTokenParams) => { return jwtClaims; } - throw new Error( - `The aud parameter does not include this client - is: ${jwtClaims.aud} | expected: ${params.clientId}` - ); + throw new Error(`The aud parameter does not include this client - is: ${jwtClaims.aud} | expected: ${params.clientId}`); }; diff --git a/src/test/verifyAppleIdToken.test.ts b/src/test/verifyAppleIdToken.test.ts index 27ae544..c2b2143 100644 --- a/src/test/verifyAppleIdToken.test.ts +++ b/src/test/verifyAppleIdToken.test.ts @@ -98,7 +98,7 @@ describe("Verify Apple idToken", () => { ); await verifyAppleIdToken({ idToken, clientId }); } catch (error) { - return expect(error.message).toMatch(/jwt expired/); + return expect(error.message).toMatch(/"exp" claim timestamp check failed/); } throw new Error("Expected to throw"); }); @@ -116,7 +116,7 @@ describe("Verify Apple idToken", () => { ); await verifyAppleIdToken({ idToken, clientId, nonce: "def" }); } catch (error) { - return expect(error.message).toMatch(/jwt nonce invalid/); + return expect(error.message).toMatch(/The nonce parameter does not match/); } throw new Error("Expected to throw"); });