Проверка токена идентификатора JWT Google OpenID Connect

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

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

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

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, но я не имею ни малейшего представления, как ее выключить. Любые идеи?


person ReimTime    schedule 20.04.2015    source источник


Ответы (4)


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

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

Дополнительную информацию см. В этой статье: Критические изменения JwtSecurityTokenHandler 4.0.0?

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

tokenValidationParameters.IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) => { return new X509SecurityKey(certificate); };
person Hans Z.    schedule 20.04.2015
comment
Как только я понял, как это сделать, это сработало отлично. Спасибо за помощь. Код выглядит так: tokenValidationParameters.IssuerSigningKeyResolver = (arbitrarily, declaring, these, parameters) => { return new X509SecurityKey(certificate); }; - person ReimTime; 21.04.2015

Я подумал, что опубликую свою слегка улучшенную версию, которая использует JSON.Net для анализа сертификатов Googles 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;
        }
    }
person Johannes Rudolph    schedule 21.04.2015
comment
Большое спасибо за ваш фрагмент кода! Мне все еще интересно, есть ли способ сгенерировать эти открытые ключи / сертификаты из ответа googleapis .com / oauth2 / v3 / certs (Пробовал с RSACryptoServiceProvider, но, к сожалению, не удалось.) - person Robar; 29.04.2015
comment
@Robar: конечная точка v1 исчезнет в ближайшее время? Еще я заметил, что Google меняет сертификаты ежедневно, поэтому вам нужно иметь дело с промахами кеша, а затем повторно получать сертификаты. - person Johannes Rudolph; 29.04.2015
comment
Надеюсь, что нет, но текущий jwks_uri документа обнаружения - это конечная точка v3 (см. учетные записи. google.com/.well-known/openid-configuration). Я уже решил проблему с чередующимися сертификатами, поместив сертификаты в кеш со сроком действия. Я получаю время истечения срока действия из HTTP-запроса, который получает сертификаты, в HTTP-ответе установлено max-age. Кроме того, я делаю одно повторное извлечение сертификатов, если проверка не удалась с первой попытки. - person Robar; 29.04.2015

Сотрудники Microsoft опубликовали образец кода для конечной точки Azure V2 B2C Preview, поддерживающей 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();
        }
    }
}
person Haroon    schedule 03.05.2016
comment
Обратите внимание, что этот код небезопасен (и приведенный выше код удален из обновленной ссылки). Вызов задач .Result внутри ReaderWriterLockSlim в конечном итоге сломает ваш сервер, и все последующие запросы будут заблокированы в ожидании снятия блокировки. Если вам нужно вызвать async из синхронизации, используйте GetAwaiter().GetResult(). Нам также все еще нужно было использовать подобный код, и мы полностью удалили ReaderWriterLockSlim, поскольку он не требуется. - person Robert Paulson; 10.08.2018

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

Это мой рабочий код сейчас:

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;
        }
    }
}
person Thomas    schedule 10.05.2017