| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | using Google.Apis.Auth.OAuth2; |
| | using Google.Apis.Util; |
| | using Newtonsoft.Json.Linq; |
| | using System; |
| | using System.Collections.Generic; |
| | using System.Linq; |
| | using System.Net.Http; |
| | using System.Security.Cryptography; |
| | using System.Threading; |
| | using System.Threading.Tasks; |
| | using static Google.Apis.Auth.JsonWebSignature; |
| |
|
| | namespace Google.Apis.Auth |
| | { |
| | internal static class SignedTokenVerification |
| | { |
| | #if NET462_OR_GREATER |
| | |
| | |
| | |
| | |
| | private static readonly byte[] s_cngBlobPrefix = { 0x45, 0x43, 0x53, 0x31, 0x20, 0, 0, 0 }; |
| | #endif |
| |
|
| | private static readonly CertificateCache s_certificateCache = new CertificateCache(); |
| |
|
| | internal async static Task VerifySignedTokenAsync<TJswHeader, TJswPayload>( |
| | SignedToken<TJswHeader, TJswPayload> signedToken, SignedTokenVerificationOptions options, CancellationToken cancellationToken) |
| | where TJswHeader : Header |
| | where TJswPayload : Payload |
| | { |
| | signedToken.ThrowIfNull(nameof(signedToken)); |
| | options = options == null ? new SignedTokenVerificationOptions() : new SignedTokenVerificationOptions(options); |
| | options.CertificateCache ??= s_certificateCache; |
| |
|
| | |
| | Task signatureVerificationTask = signedToken.Header.Algorithm switch |
| | { |
| | "RS256" => VerifyRS256TokenAsync(signedToken, options, cancellationToken), |
| | "ES256" => VerifyES256TokenAsync(signedToken, options, cancellationToken), |
| | _ => throw new InvalidJwtException("Signing algorithm must be either RS256 or ES256.") |
| | }; |
| |
|
| | |
| |
|
| | |
| | if (options.TrustedIssuers.Count > 0 && !options.TrustedIssuers.Contains(signedToken.Payload.Issuer)) |
| | { |
| | var validList = string.Join(", ", options.TrustedIssuers.Select(x => $"'{x}'")); |
| | throw new InvalidJwtException($"JWT issuer incorrect. Must be one of: {validList}"); |
| | } |
| | |
| | if (options.TrustedAudiences.Count > 0 && signedToken.Payload.AudienceAsList.Except(options.TrustedAudiences).Any()) |
| | { |
| | throw new InvalidJwtException("JWT contains untrusted 'aud' claim."); |
| | } |
| | |
| | DateTimeOffset issuedAt = signedToken.Payload.IssuedAt ?? throw new InvalidJwtException("JWT must contain 'iat' and 'exp' claims"); |
| | DateTimeOffset expiresAt = signedToken.Payload.ExpiresAt ?? throw new InvalidJwtException("JWT must contain 'iat' and 'exp' claims"); |
| |
|
| | |
| | var utcNow = options.Clock.UtcNow; |
| | if (utcNow + options.IssuedAtClockTolerance < issuedAt) |
| | { |
| | throw new InvalidJwtException("JWT is not yet valid."); |
| | } |
| | |
| | if (utcNow - options.ExpiryClockTolerance > expiresAt) |
| | { |
| | throw new InvalidJwtException("JWT has expired."); |
| | } |
| |
|
| | |
| | await signatureVerificationTask.ConfigureAwait(false); |
| | } |
| |
|
| | private async static Task VerifyRS256TokenAsync<TJswHeader, TJswPayload>( |
| | SignedToken<TJswHeader, TJswPayload> signedToken, SignedTokenVerificationOptions options, CancellationToken cancellationToken) |
| | where TJswHeader : Header |
| | where TJswPayload : Payload |
| | { |
| | var certificates = await GetCertificatesAsync( |
| | options, GoogleAuthConsts.JsonWebKeySetUrl, FromKeyToRsa, cancellationToken).ConfigureAwait(false); |
| |
|
| | foreach (RSA certificate in certificates) |
| | { |
| | if (certificate.VerifyHash(signedToken.Sha256Hash, signedToken.Signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)) |
| | { |
| | return; |
| | } |
| | } |
| | throw new InvalidJwtException("JWT invalid, unable to verify signature."); |
| | } |
| |
|
| | |
| | internal static RSA FromKeyToRsa(JToken key) |
| | { |
| | var rsa = RSA.Create(); |
| | rsa.ImportParameters(new RSAParameters |
| | { |
| | Modulus = TokenEncodingHelpers.Base64UrlDecode((string)key["n"]), |
| | Exponent = TokenEncodingHelpers.Base64UrlDecode((string)key["e"]), |
| | }); |
| | return rsa; |
| | } |
| |
|
| | private async static Task VerifyES256TokenAsync<TJswHeader, TJswPayload>( |
| | SignedToken<TJswHeader, TJswPayload> signedToken, SignedTokenVerificationOptions options, CancellationToken cancellationToken) |
| | where TJswHeader : Header |
| | where TJswPayload : Payload |
| | { |
| | var certificates = await GetCertificatesAsync( |
| | options, GoogleAuthConsts.IapKeySetUrl, FromKeyToECDsa, cancellationToken).ConfigureAwait(false); |
| |
|
| | foreach (ECDsa certificate in certificates) |
| | { |
| | if (certificate.VerifyHash(signedToken.Sha256Hash, signedToken.Signature)) |
| | { |
| | return; |
| | } |
| | } |
| | throw new InvalidJwtException("JWT invalid, unable to verify signature."); |
| |
|
| | static ECDsa FromKeyToECDsa(JToken key) |
| | { |
| | if ((string)key["kty"] != "EC" && (string)key["crv"] != "P-256") |
| | { |
| | throw new ArgumentException( |
| | $"For ES256 verification only certificates with kty='EC' and crv='P-256' are supported. Encountered: kty={(string)key["kty"]} and crv={(string)key["crv"]}."); |
| | } |
| | byte[] x = TokenEncodingHelpers.Base64UrlDecode((string)key["x"]); |
| | byte[] y = TokenEncodingHelpers.Base64UrlDecode((string)key["y"]); |
| | return BuildEcdsa(x, y); |
| | } |
| | #if NETSTANDARD2_0 || NET6_0_OR_GREATER |
| | static ECDsa BuildEcdsa(byte[] x, byte[] y) |
| | { |
| | var ecdsa = ECDsa.Create(); |
| | ecdsa.ImportParameters(new ECParameters |
| | { |
| | |
| | Curve = ECCurve.NamedCurves.nistP256, |
| | Q = new ECPoint { X = x, Y = y } |
| | }); |
| | return ecdsa; |
| | } |
| | #elif NET462 |
| | static ECDsa BuildEcdsa(byte[] x, byte[] y) |
| | { |
| | |
| | |
| | |
| | |
| | byte[] publicKey = new byte[s_cngBlobPrefix.Length + 64]; |
| | Buffer.BlockCopy(s_cngBlobPrefix, 0, publicKey, 0, s_cngBlobPrefix.Length); |
| | Buffer.BlockCopy(x, 0, publicKey, s_cngBlobPrefix.Length, x.Length); |
| | Buffer.BlockCopy(y, 0, publicKey, s_cngBlobPrefix.Length + x.Length, y.Length); |
| | return new ECDsaCng(CngKey.Import(publicKey, CngKeyBlobFormat.EccPublicBlob)); |
| | } |
| | #endif |
| | } |
| |
|
| | private static async Task<IEnumerable<AsymmetricAlgorithm>> GetCertificatesAsync( |
| | SignedTokenVerificationOptions options, |
| | string defaultCertificateLocation, |
| | Func<JToken, AsymmetricAlgorithm> certificateFactory, |
| | CancellationToken cancellationToken) => |
| | await options.CertificateCache.GetCertificatesAsync( |
| | options.CertificatesUrl ?? defaultCertificateLocation, |
| | certificateFactory, |
| | options.ForceCertificateRefresh, |
| | cancellationToken).ConfigureAwait(false); |
| |
|
| | |
| | internal abstract class CertificateCacheBase |
| | { |
| | internal static readonly TimeSpan DefaultCacheValidity = TimeSpan.FromHours(1); |
| |
|
| | private readonly IDictionary<string, CachedCertificates> _cache = new Dictionary<string, CachedCertificates>(); |
| | private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1); |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | private readonly IClock _clock; |
| |
|
| | |
| | |
| | protected CertificateCacheBase(IClock clock) => |
| | _clock = clock ?? SystemClock.Default; |
| |
|
| | public async Task<IEnumerable<AsymmetricAlgorithm>> GetCertificatesAsync(string certificatesLocation, Func<JToken, AsymmetricAlgorithm> certificateFactory, bool forceCertificateRefresh, CancellationToken cancellationToken) |
| | { |
| | certificatesLocation.ThrowIfNullOrEmpty(nameof(certificatesLocation)); |
| | certificateFactory.ThrowIfNull(nameof(certificateFactory)); |
| |
|
| | await _cacheLock.WaitAsync(cancellationToken).ConfigureAwait(false); |
| | try |
| | { |
| | if (forceCertificateRefresh || |
| | !_cache.TryGetValue(certificatesLocation, out CachedCertificates cachedCerts) || |
| | cachedCerts.Expired(_clock.UtcNow)) |
| | { |
| | string certificatesJson = await FetchCertificatesAsync(certificatesLocation).ConfigureAwait(false); |
| | IEnumerable<JToken> jwks = JToken.Parse(certificatesJson)["keys"]?.AsEnumerable() |
| | ?? throw new ArgumentException($"Only JWK formatted keys are currently supported. No 'keys' element was found in {certificatesLocation}"); |
| | if (!jwks.Any()) |
| | { |
| | throw new ArgumentException($"No JWKs were found on {certificatesLocation}. The 'keys' element was empty."); |
| | } |
| | cachedCerts = new CachedCertificates(jwks.Select(key => certificateFactory(key)).ToList(), _clock.UtcNow); |
| | _cache[certificatesLocation] = cachedCerts; |
| | } |
| | return cachedCerts.Certificates; |
| | } |
| | finally |
| | { |
| | _cacheLock.Release(); |
| | } |
| | } |
| |
|
| | |
| | protected abstract Task<string> FetchCertificatesAsync(string certificatesLocation); |
| |
|
| | internal struct CachedCertificates |
| | { |
| | private readonly DateTimeOffset _cachedSince; |
| | public IEnumerable<AsymmetricAlgorithm> Certificates { get; } |
| |
|
| | public CachedCertificates(IEnumerable<AsymmetricAlgorithm> certificates, DateTimeOffset cachedSince) |
| | { |
| | _cachedSince = cachedSince; |
| | Certificates = certificates; |
| | } |
| |
|
| | public bool Expired(DateTimeOffset now) => _cachedSince + DefaultCacheValidity <= now; |
| | } |
| | } |
| |
|
| | internal sealed class CertificateCache : CertificateCacheBase |
| | { |
| | public CertificateCache() : base(SystemClock.Default) |
| | { } |
| |
|
| | protected override async Task<string> FetchCertificatesAsync(string certificatesLocation) |
| | { |
| | using var httpClient = new HttpClient(); |
| | return await httpClient.GetStringAsync(certificatesLocation).ConfigureAwait(false); |
| | } |
| | } |
| | } |
| | } |
| |
|