/* Copyright 2017 Google Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ using Google.Apis.Auth.OAuth2; using Google.Apis.Json; 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.Text; using System.Threading; using System.Threading.Tasks; namespace Google.Apis.Auth { /// /// Google JSON Web Signature as specified in https://developers.google.com/accounts/docs/OAuth2ServiceAccount. /// public class GoogleJsonWebSignature { internal const int MaxJwtLength = 10000; internal readonly static TimeSpan CertCacheRefreshInterval = TimeSpan.FromHours(1); // See http://oid-info.com/get/2.16.840.1.101.3.4.2.1 private const string Sha256Oid = "2.16.840.1.101.3.4.2.1"; private const string SupportedJwtAlgorithm = "RS256"; private static readonly IEnumerable ValidJwtIssuers = new[] { "https://accounts.google.com", "accounts.google.com" }; private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); /// /// Validates a Google-issued Json Web Token (JWT). /// With throw a if the passed value is not valid JWT signed by Google. /// /// /// Follows the procedure to /// validate a JWT ID token. /// /// Google certificates are cached, and refreshed once per hour. This can be overridden by setting /// to true. /// /// The JWT to validate. /// Optional. The to use for JWT expiration verification. Defaults to the system clock. /// Optional. If true forces new certificates to be downloaded from Google. Defaults to false. /// The JWT payload, if the JWT is valid. Throws an otherwise. /// Thrown when passed a JWT that is not a valid JWT signed by Google. public static Task ValidateAsync(string jwt, IClock clock = null, bool forceGoogleCertRefresh = false) => ValidateInternalAsync(jwt, clock ?? SystemClock.Default, forceGoogleCertRefresh, null); // internal for testing internal static async Task ValidateInternalAsync(string jwt, IClock clock, bool forceGoogleCertRefresh, string certsJson) { // Check arguments jwt.ThrowIfNull(nameof(jwt)); jwt.ThrowIfNullOrEmpty(nameof(jwt)); if (jwt.Length > MaxJwtLength) { throw new InvalidJwtException($"JWT exceeds maximum allowed length of {MaxJwtLength}"); } var parts = jwt.Split('.'); if (parts.Length != 3) { throw new InvalidJwtException($"JWT must consist of Header, Payload, and Signature"); } // Decode the three parts of the JWT: header.payload.signature Header header = NewtonsoftJsonSerializer.Instance.Deserialize
(Base64UrlToString(parts[0])); Payload payload = NewtonsoftJsonSerializer.Instance.Deserialize(Base64UrlToString(parts[1])); byte[] signature = Base64UrlDecode(parts[2]); // Verify algorithm in JWT if (header.Algorithm != SupportedJwtAlgorithm) { throw new InvalidJwtException($"JWT algorithm must be '{SupportedJwtAlgorithm}'"); } // Verify signature byte[] hash; using (var hashAlg = SHA256.Create()) { hash = hashAlg.ComputeHash(Encoding.ASCII.GetBytes($"{parts[0]}.{parts[1]}")); } bool verifiedOk = false; foreach (var googleCert in await GetGoogleCertsAsync(clock, forceGoogleCertRefresh, certsJson)) { #if NET45 verifiedOk = ((RSACryptoServiceProvider)googleCert).VerifyHash(hash, Sha256Oid, signature); #elif DNX451 verifiedOk = ((RSACryptoServiceProvider)googleCert).VerifyHash(hash, Sha256Oid, signature); #elif NETSTANDARD1_3 verifiedOk = googleCert.VerifyHash(hash, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); #else #error Unsupported platform #endif if (verifiedOk) { break; } } if (!verifiedOk) { throw new InvalidJwtException("JWT invalid: unable to verify signature."); } // Verify iss, iat and exp claims if (!ValidJwtIssuers.Contains(payload.Issuer)) { var validList = string.Join(", ", ValidJwtIssuers.Select(x => $"'{x}'")); throw new InvalidJwtException($"JWT issuer incorrect. Must be one of: {validList}"); } if (payload.IssuedAtTimeSeconds == null || payload.ExpirationTimeSeconds == null) { throw new InvalidJwtException("JWT must contain 'iat' and 'exp' claims"); } var nowSeconds = (clock.UtcNow - UnixEpoch).TotalSeconds; if (nowSeconds < payload.IssuedAtTimeSeconds.Value) { throw new InvalidJwtException("JWT is not yet valid."); } if (nowSeconds > payload.ExpirationTimeSeconds.Value) { throw new InvalidJwtException("JWT has expired."); } // All verification passed, return payload. return payload; } private static string Base64UrlToString(string base64Url) => Encoding.UTF8.GetString(Base64UrlDecode(base64Url)); private static byte[] Base64UrlDecode(string base64Url) { var base64 = base64Url.Replace('-', '+').Replace('_', '/'); switch (base64.Length % 4) { case 2: base64 += "=="; break; case 3: base64 += "="; break; } return Convert.FromBase64String(base64); } private static SemaphoreSlim _certCacheLock = new SemaphoreSlim(1); private static DateTime _certCacheDownloadTime; private static List _certCache; // internal for testing internal static async Task> GetGoogleCertsAsync(IClock clock, bool forceGoogleCertRefresh, string certsJson) { var now = clock.UtcNow; await _certCacheLock.WaitAsync(); try { if (forceGoogleCertRefresh || _certCache == null || (_certCacheDownloadTime + CertCacheRefreshInterval) < now) { using (var httpClient = new HttpClient()) { // certsJson used for unit tests if (certsJson == null) { certsJson = await httpClient.GetStringAsync(GoogleAuthConsts.JsonWebKeySetUrl); } } _certCache = GetGoogleCertsFromJson(certsJson); _certCacheDownloadTime = now; } return _certCache; } finally { _certCacheLock.Release(); } } private static List GetGoogleCertsFromJson(string json) => JToken.Parse(json)["keys"].AsEnumerable().Select(key => { var rsa = RSA.Create(); rsa.ImportParameters(new RSAParameters { Modulus = Base64UrlDecode((string)key["n"]), Exponent = Base64UrlDecode((string)key["e"]), }); return rsa; }) .ToList(); /// /// The header as specified in https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingheader. /// public class Header : JsonWebSignature.Header { } /// /// The payload as specified in /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset. /// public class Payload : JsonWebSignature.Payload { /// /// a space-delimited list of the permissions the application requests or null. /// [Newtonsoft.Json.JsonPropertyAttribute("scope")] public string Scope { get; set; } /// /// The email address of the user for which the application is requesting delegated access. /// [Newtonsoft.Json.JsonPropertyAttribute("prn")] public string Prn { get; set; } } } }