Validates the JWT Tokens send by apple during authentication
using System; | |
using System.Collections.Generic; | |
using System.IdentityModel.Tokens.Jwt; | |
using System.Linq; | |
using System.Net.Http; | |
using System.Text.Json; | |
using System.Threading.Tasks; | |
using Microsoft.IdentityModel.Tokens; | |
/// <summary> | |
/// Validates the JWT Tokens send by apple during authentication | |
/// </summary> | |
public class AppleTokenValidator : IDisposable | |
{ | |
private const string ApplePublicKeysEndpoint = "https://appleid.apple.com/auth/keys"; | |
private readonly HttpClient _httpClient; | |
/// <summary> | |
/// Initializes a new instance of the <see cref="AppleTokenValidator"/> class. | |
/// </summary> | |
public AppleTokenValidator() : this(new HttpClient()) { } | |
/// <summary> | |
/// Initializes a new instance of the <see cref="AppleTokenValidator"/> class. | |
/// </summary> | |
public AppleTokenValidator(HttpClient httpClient) | |
{ | |
_httpClient = httpClient; | |
} | |
/// <summary> | |
/// Validates a Apple-issued Json Web Token (JWT). | |
/// </summary> | |
/// <exception cref="HttpRequestException">If Apple doesn't return an associated public key.</exception> | |
/// <exception cref="ArgumentNullException"></exception> | |
/// <exception cref="ArgumentException"></exception> | |
/// <exception cref="SecurityTokenDecryptionFailedException"></exception> | |
/// <exception cref="SecurityTokenEncryptionKeyNotFoundException"></exception> | |
/// <exception cref="SecurityTokenException"></exception> | |
/// <exception cref="SecurityTokenExpiredException"></exception> | |
/// <exception cref="SecurityTokenInvalidAudienceException"></exception> | |
/// <exception cref="SecurityTokenInvalidLifetimeException"></exception> | |
/// <exception cref="SecurityTokenInvalidSignatureException"></exception> | |
/// <exception cref="SecurityTokenNoExpirationException"></exception> | |
/// <exception cref="SecurityTokenNotYetValidException"></exception> | |
/// <exception cref="SecurityTokenReplayAddFailedException"></exception> | |
/// <exception cref="SecurityTokenReplayDetectedException"></exception> | |
public async Task<SecurityToken> ValidateAsync(string token) | |
{ | |
if (string.IsNullOrWhiteSpace(token)) | |
{ | |
throw new ArgumentException("token is required"); | |
} | |
var response = await _httpClient.GetAsync(ApplePublicKeysEndpoint); | |
if (!response.IsSuccessStatusCode) | |
{ | |
throw new HttpRequestException(await response.Content.ReadAsStringAsync()); | |
} | |
var applePublicKeys = await JsonSerializer.DeserializeAsync<AppleKeys>(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); | |
var kid = new JwtSecurityTokenHandler().ReadJwtToken(token).Header.Kid; | |
var publicKey = applePublicKeys.Keys.FirstOrDefault(key => key.Kid == kid); | |
if (publicKey == null) | |
{ | |
throw new Exception($"kid {kid} not found in apple public keys"); | |
} | |
var tokenHandler = new JwtSecurityTokenHandler(); | |
var validationParameters = new TokenValidationParameters | |
{ | |
IssuerSigningKey = publicKey, | |
ValidateAudience = false, | |
ValidateIssuer = false, | |
}; | |
tokenHandler.ValidateToken(token, validationParameters, out var validatedToken); | |
return validatedToken; | |
} | |
public void Dispose() | |
{ | |
_httpClient?.Dispose(); | |
} | |
} | |
public class AppleKeys | |
{ | |
public IReadOnlyCollection<JsonWebKey> Keys { get; set; } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment