Apple Sign-in - Validating ID Token

Viewed 1025

I am implementing Apple Sign-in in Mobile app. After successful sign-in, I get the ID token which is valid for one day.

Here is sample JWT token which I received after sign-in:

eyJraWQiOiJZdXlYb1kiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmNsdWJiYWJsZSIsImV4cCI6MTYxODU4NTcyOCwiaWF0IjoxNjE4NDk5MzI4LCJzdWIiOiIwMDE4NTAuMzdkY2NmZGIxYzEyNDliY2E2NjE5YThkYjQ2MWFlNDkuMDQzMyIsImNfaGFzaCI6InVMTUV1eTRCaFozRVc1NXR1OXZtZGciLCJhdXRoX3RpbWUiOjE2MTg0OTkzMjgsIm5vbmNlX3N1cHBvcnRlZCI6dHJ1ZX0.e86uz4Qqu63mD0hHVdzBU3EfW0G-rDUprBiXkyPkvPIHIWPM1LyjpFs2GeoWAcdfUdmGww6C8SLLgk4iMKjK_yrpxiQBmbIzdCLBrwW4P8Y40llcrotyRuwyalfycyUJ8GP9yqjs5_R7yZlDd4oq0wrDNyXVjlbdGSfNUGqmXScBgXm3yCH0rD85GK0hX3XM-fA133Y5tj1DRALZhnw2GLy-6YEPBlqFE-cvu9aif9ajuDx3gPwp9AQ_nXP3pWjSg2G5eYx7UMpowXbAVoDSlhQVu_KJgxPsW61i50QnhykmeNA7LxA2iLQnlGk5VKzNlATub49SybUnmPSViO_Fbw

Now I want to validate this token using JwtSecurityTokenHandler available under System.IdentityModel.Tokens.Jwt.

I wrote following code:

tokenHandler.ValidateToken(body.access_token, new TokenValidationParameters() 
{ 
    ValidateIssuer = true,
    ValidIssuers = new List<string> { appleTokenIssuerName },
    ValidateAudience = true,
    ValidAudiences =  new List<string> { appleAppClientId },
    ValidateLifetime = false,
    ValidateIssuerSigningKey = true,
    IssuerSigningKey = // I want to pass signing key here but I'm not sure how can I do that.
}, out SecurityToken token);

Apple signing keys are available as Json web key set here - https://appleid.apple.com/auth/keys

Can you help me find correct way to supply this key to SecurityKey parameter?

2 Answers

The simplest way would be to use JsonWebKey and pass to the constructor a JSON with one of the keys. A more elegant way would be to create a resolver, which could download those keys from Apple when needed. This will also enable you to create fallbacks and refresh keys from Apple when they rotate.

IssuerSigningKey expects an object derived from SecurityKey. Examples of this are JsonWebKey or RsaSecurityKey.

Since the posted link contains public keys in JWK format, JsonWebKey (as already mentioned in the other answer) is a close solution:

using Microsoft.IdentityModel.Tokens;
...
string jwkSerialized = @"{
                        ""kty"":""RSA"",
                        ""kid"":""YuyXoY"",
                        ""use"":""sig"",
                        ""alg"":""RS256"",
                        ""n"":""1JiU4l3YCeT4o0gVmxGTEK1IXR-Ghdg5Bzka12tzmtdCxU00ChH66aV-4HRBjF1t95IsaeHeDFRgmF0lJbTDTqa6_VZo2hc0zTiUAsGLacN6slePvDcR1IMucQGtPP5tGhIbU-HKabsKOFdD4VQ5PCXifjpN9R-1qOR571BxCAl4u1kUUIePAAJcBcqGRFSI_I1j_jbN3gflK_8ZNmgnPrXA0kZXzj1I7ZHgekGbZoxmDrzYm2zmja1MsE5A_JX7itBYnlR41LOtvLRCNtw7K3EFlbfB6hkPL-Swk5XNGbWZdTROmaTNzJhV-lWT0gGm6V1qWAK2qOZoIDa_3Ud0Gw"",
                        ""e"":""AQAB""
                        }";
JsonWebKey jwk = new JsonWebKey(jwkSerialized);

The key matching the posted JWT can be identified by the kid contained in the header of the JWT, e.g. using https://jwt.io/.

As said before, the import is not limited to the JWK format. E.g. if the key is in the widely used X.509/SPKI format, it can be imported (e.g. under .NET Core 3.0+) as follows:

using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
...
byte[] x509der = Convert.FromBase64String("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1JiU4l3YCeT4o0gVmxGTEK1IXR+Ghdg5Bzka12tzmtdCxU00ChH66aV+4HRBjF1t95IsaeHeDFRgmF0lJbTDTqa6/VZo2hc0zTiUAsGLacN6slePvDcR1IMucQGtPP5tGhIbU+HKabsKOFdD4VQ5PCXifjpN9R+1qOR571BxCAl4u1kUUIePAAJcBcqGRFSI/I1j/jbN3gflK/8ZNmgnPrXA0kZXzj1I7ZHgekGbZoxmDrzYm2zmja1MsE5A/JX7itBYnlR41LOtvLRCNtw7K3EFlbfB6hkPL+Swk5XNGbWZdTROmaTNzJhV+lWT0gGm6V1qWAK2qOZoIDa/3Ud0GwIDAQAB");
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportSubjectPublicKeyInfo(x509der, out _); 
RsaSecurityKey rsask = new RsaSecurityKey(rsa);

The equivalence of both keys can be verified e.g. here.

The posted JWT can then be verified as follows using either jwk or rsask:

using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
...
bool verified = false;
string jwt = "eyJraWQiOiJZdXlYb1kiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmNsdWJiYWJsZSIsImV4cCI6MTYxODU4NTcyOCwiaWF0IjoxNjE4NDk5MzI4LCJzdWIiOiIwMDE4NTAuMzdkY2NmZGIxYzEyNDliY2E2NjE5YThkYjQ2MWFlNDkuMDQzMyIsImNfaGFzaCI6InVMTUV1eTRCaFozRVc1NXR1OXZtZGciLCJhdXRoX3RpbWUiOjE2MTg0OTkzMjgsIm5vbmNlX3N1cHBvcnRlZCI6dHJ1ZX0.e86uz4Qqu63mD0hHVdzBU3EfW0G-rDUprBiXkyPkvPIHIWPM1LyjpFs2GeoWAcdfUdmGww6C8SLLgk4iMKjK_yrpxiQBmbIzdCLBrwW4P8Y40llcrotyRuwyalfycyUJ8GP9yqjs5_R7yZlDd4oq0wrDNyXVjlbdGSfNUGqmXScBgXm3yCH0rD85GK0hX3XM-fA133Y5tj1DRALZhnw2GLy-6YEPBlqFE-cvu9aif9ajuDx3gPwp9AQ_nXP3pWjSg2G5eYx7UMpowXbAVoDSlhQVu_KJgxPsW61i50QnhykmeNA7LxA2iLQnlGk5VKzNlATub49SybUnmPSViO_Fbw";
var tokenHandler = new JwtSecurityTokenHandler();
try
{
    tokenHandler.ValidateToken(
        jwt,
        new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuers = new List<string> { "https://appleid.apple.com" },
            ValidateAudience = true,
            ValidAudiences = new List<string> { "com.clubbable" },
            ValidateLifetime = false,
            IssuerSigningKey = jwk // alternatively, rsask can be used            
        },
        out SecurityToken token);
    verified = true;
    Console.WriteLine("Token: " + token.ToString());
}
catch (Exception ex)
{
    Console.WriteLine("Verification failure: " + ex.Message);
}

Console.WriteLine("Verified: " + verified);
Related