Проверка Google OpenID Connect JWT ID Token

Я пытаюсь обновить свой веб-сайт MVC, чтобы использовать новый стандарт OpenID Connect. Средство промежуточного ПО OWIN кажется довольно надежным, но, к сожалению, оно поддерживает только Тип ответа "form_post". Это означает, что Google несовместим, поскольку он возвращает все токены в URL-адресе после "#", поэтому они никогда не доходят до сервера и никогда не запускают промежуточное программное обеспечение.

Я пытался запускать обработчики ответов в промежуточном программном обеспечении самостоятельно, но это, похоже, не работает вообще, поэтому у меня есть просто файл javascript, который анализирует возвращенные претензии и POSTs их на действие контроллера для обработки.

Проблема в том, что даже когда я получаю их на стороне сервера, я не могу их правильно разобрать. Ошибка, которую я получаю, выглядит следующим образом:

IDX10500: Signature validation failed. Unable to resolve     
SecurityKeyIdentifier: 'SecurityKeyIdentifier
(
   IsReadOnly = False,
   Count = 1,
   Clause[0] = System.IdentityModel.Tokens.NamedKeySecurityKeyIdentifierClause
),
token: '{
    "alg":"RS256",
    "kid":"073a3204ec09d050f5fd26460d7ddaf4b4ec7561"
}.
{
    "iss":"accounts.google.com",
    "sub":"100330116539301590598",
    "azp":"1061880999501-b47blhmmeprkvhcsnqmhfc7t20gvlgfl.apps.googleusercontent.com",
    "nonce":"7c8c3656118e4273a397c7d58e108eb1",
    "email_verified":true,
    "aud":"1061880999501-b47blhmmeprkvhcsnqmhfc7t20gvlgfl.apps.googleusercontent.com",
    "iat":1429556543,"exp\":1429560143
    }'."
}

Мой код проверки токена следует примеру, показанному хорошими людьми, разрабатывающими IdentityServer

    private async Task<IEnumerable<Claim>> ValidateIdentityTokenAsync(string idToken, string state)
    {
        // New Stuff
        var token = new JwtSecurityToken(idToken);
        var jwtHandler = new JwtSecurityTokenHandler();
        byte[][] certBytes = getGoogleCertBytes();

        for (int i = 0; i < certBytes.Length; i++)
        {
            var certificate = new X509Certificate2(certBytes[i]);
            var certToken = new X509SecurityToken(certificate);

            // Set up token validation
            var tokenValidationParameters = new TokenValidationParameters();
            tokenValidationParameters.ValidAudience = googleClientId;
            tokenValidationParameters.IssuerSigningToken = certToken;
            tokenValidationParameters.ValidIssuer = "accounts.google.com";

            try
            {
                // Validate
                SecurityToken jwt;
                var claimsPrincipal = jwtHandler.ValidateToken(idToken, tokenValidationParameters, out jwt);
                if (claimsPrincipal != null)
                {
                    // Valid
                    idTokenStatus = "Valid";
                }
            }
            catch (Exception e)
            {
                if (idTokenStatus != "Valid")
                {
                    // Invalid?

                }
            }
        }

        return token.Claims;
    }

    private byte[][] getGoogleCertBytes()
    {
        // The request will be made to the authentication server.
        WebRequest request = WebRequest.Create(
            "https://www.googleapis.com/oauth2/v1/certs"
        );

        StreamReader reader = new StreamReader(request.GetResponse().GetResponseStream());

        string responseFromServer = reader.ReadToEnd();

        String[] split = responseFromServer.Split(':');

        // There are two certificates returned from Google
        byte[][] certBytes = new byte[2][];
        int index = 0;
        UTF8Encoding utf8 = new UTF8Encoding();
        for (int i = 0; i < split.Length; i++)
        {
            if (split[i].IndexOf(beginCert) > 0)
            {
                int startSub = split[i].IndexOf(beginCert);
                int endSub = split[i].IndexOf(endCert) + endCert.Length;
                certBytes[index] = utf8.GetBytes(split[i].Substring(startSub, endSub).Replace("\\n", "\n"));
                index++;
            }
        }
        return certBytes;
    }

Я знаю, что проверка подлинности подписи не является полностью необходимой для JWT, но я не имею ни малейшего представления о том, как отключить ее. Любые идеи?

Ответ 1

Проблема заключается в kid в JWT, значение которого является идентификатором ключа, который использовался для подписания JWT. Поскольку вы вручную создаете массив сертификатов из URI JWK, вы теряете информацию идентификатора ключа. Однако процедура проверки требует этого.

Вам нужно будет установить tokenValidationParameters.IssuerSigningKeyResolver на функцию, которая будет возвращать ту же клавишу, которую вы указали выше в tokenValidationParameters.IssuerSigningToken. Цель этого делегата - дать команду среде выполнения игнорировать любую "совпадающую" семантику и просто попробовать ключ.

См. эту статью для получения дополнительной информации: JwtSecurityTokenHandler 4.0.0 Срочные изменения?

Изменение: код:

tokenValidationParameters.IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) => { return new X509SecurityKey(certificate); };

Ответ 2

Я думал, что разместил бы мою улучшенную версию, которая использует JSON.Net для разбора сертификатов Google X509 и соответствует используемому ключу на основе "kid" (key-id). Это немного эффективнее, чем попробовать каждый сертификат, так как асимметричный крипторежим обычно довольно дорог.

Также удалены устаревшие WebClient и ручной синтаксический код строки:

    static Lazy<Dictionary<string, X509Certificate2>> Certificates = new Lazy<Dictionary<string, X509Certificate2>>( FetchGoogleCertificates );
    static Dictionary<string, X509Certificate2> FetchGoogleCertificates()
    {
        using (var http = new HttpClient())
        {
            var json = http.GetStringAsync( "https://www.googleapis.com/oauth2/v1/certs" ).Result;

            var dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>( json );
            return dictionary.ToDictionary( x => x.Key, x => new X509Certificate2( Encoding.UTF8.GetBytes( x.Value ) ) );
        }
    }

    JwtSecurityToken ValidateIdentityToken( string idToken )
    {
        var token = new JwtSecurityToken( idToken );
        var jwtHandler = new JwtSecurityTokenHandler();

        var certificates = Certificates.Value;

        try
        {
            // Set up token validation
            var tokenValidationParameters = new TokenValidationParameters();
            tokenValidationParameters.ValidAudience = _clientId;
            tokenValidationParameters.ValidIssuer = "accounts.google.com";
            tokenValidationParameters.IssuerSigningTokens = certificates.Values.Select( x => new X509SecurityToken( x ) );
            tokenValidationParameters.IssuerSigningKeys = certificates.Values.Select( x => new X509SecurityKey( x ) );
            tokenValidationParameters.IssuerSigningKeyResolver = ( s, securityToken, identifier, parameters ) =>
            {
                return identifier.Select( x =>
                {
                    if (!certificates.ContainsKey( x.Id ))
                        return null;

                    return new X509SecurityKey( certificates[ x.Id ] );
                } ).First( x => x != null );
            };

            SecurityToken jwt;
            var claimsPrincipal = jwtHandler.ValidateToken( idToken, tokenValidationParameters, out jwt );
            return (JwtSecurityToken)jwt;
        }
        catch (Exception ex)
        {
            _trace.Error( typeof( GoogleOAuth2OpenIdHybridClient ).Name, ex );
            return null;
        }
    }

Ответ 3

Люди в Microsoft отправили образец кода для конечной точки предварительного просмотра Azure V2 B2C, поддерживающей OpenId Connect. См. здесь, со вспомогательным классом OpenIdConnectionCachingSecurityTokenProvider код упрощается следующим образом:

app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
    AccessTokenFormat = new JwtFormat(new TokenValidationParameters
    {
       ValidAudiences = new[] { googleClientId },
    }, new OpenIdConnectCachingSecurityTokenProvider("https://accounts.google.com/.well-known/openid-configuration"))});

Этот класс необходим, поскольку промежуточное ПО OAuthBearer не использует рычаги. Конечная точка метаданных OpenID Connect, отображаемая по умолчанию STS.

public class OpenIdConnectCachingSecurityTokenProvider : IIssuerSecurityTokenProvider
{
    public ConfigurationManager<OpenIdConnectConfiguration> _configManager;
    private string _issuer;
    private IEnumerable<SecurityToken> _tokens;
    private readonly string _metadataEndpoint;

    private readonly ReaderWriterLockSlim _synclock = new ReaderWriterLockSlim();

    public OpenIdConnectCachingSecurityTokenProvider(string metadataEndpoint)
    {
        _metadataEndpoint = metadataEndpoint;
        _configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint);

        RetrieveMetadata();
    }

    /// <summary>
    /// Gets the issuer the credentials are for.
    /// </summary>
    /// <value>
    /// The issuer the credentials are for.
    /// </value>
    public string Issuer
    {
        get
        {
            RetrieveMetadata();
            _synclock.EnterReadLock();
            try
            {
                return _issuer;
            }
            finally
            {
                _synclock.ExitReadLock();
            }
        }
    }

    /// <summary>
    /// Gets all known security tokens.
    /// </summary>
    /// <value>
    /// All known security tokens.
    /// </value>
    public IEnumerable<SecurityToken> SecurityTokens
    {
        get
        {
            RetrieveMetadata();
            _synclock.EnterReadLock();
            try
            {
                return _tokens;
            }
            finally
            {
                _synclock.ExitReadLock();
            }
        }
    }

    private void RetrieveMetadata()
    {
        _synclock.EnterWriteLock();
        try
        {
            OpenIdConnectConfiguration config = _configManager.GetConfigurationAsync().Result;
            _issuer = config.Issuer;
            _tokens = config.SigningTokens;
        }
        finally
        {
            _synclock.ExitWriteLock();
        }
    }
}

Ответ 4

Основываясь на ответе от Йоханнеса Рудольфа, я отправляю свое решение. Ошибка в компиляторе в Департаменте DelegerSigningKeyResolver, который мне пришлось решить.

Теперь это мой рабочий код:

using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

namespace QuapiNet.Service
{
    public class JwtTokenValidation
    {
        public async Task<Dictionary<string, X509Certificate2>> FetchGoogleCertificates()
        {
            using (var http = new HttpClient())
            {
                var response = await http.GetAsync("https://www.googleapis.com/oauth2/v1/certs");

                var dictionary = await response.Content.ReadAsAsync<Dictionary<string, string>>();
                return dictionary.ToDictionary(x => x.Key, x => new X509Certificate2(Encoding.UTF8.GetBytes(x.Value)));
            }
        }

        private string CLIENT_ID = "xxxxx.apps.googleusercontent.com";

        public async Task<ClaimsPrincipal> ValidateToken(string idToken)
        {
            var certificates = await this.FetchGoogleCertificates();

            TokenValidationParameters tvp = new TokenValidationParameters()
            {
                ValidateActor = false, // check the profile ID

                ValidateAudience = true, // check the client ID
                ValidAudience = CLIENT_ID,

                ValidateIssuer = true, // check token came from Google
                ValidIssuers = new List<string> { "accounts.google.com", "https://accounts.google.com" },

                ValidateIssuerSigningKey = true,
                RequireSignedTokens = true,
                IssuerSigningKeys = certificates.Values.Select(x => new X509SecurityKey(x)),
                IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) =>
                {
                    return certificates
                    .Where(x => x.Key.ToUpper() == kid.ToUpper())
                    .Select(x => new X509SecurityKey(x.Value));
                },
                ValidateLifetime = true,
                RequireExpirationTime = true,
                ClockSkew = TimeSpan.FromHours(13)
            };

            JwtSecurityTokenHandler jsth = new JwtSecurityTokenHandler();
            SecurityToken validatedToken;
            ClaimsPrincipal cp = jsth.ValidateToken(idToken, tvp, out validatedToken);

            return cp;
        }
    }
}