Implementing Google Account as Open ID Connect in .Net

510 views Asked by At

I am trying to implement Google Account As Open ID Connect Identity Provider using .Net according following links provided by google:

Google OIDC EndPoints

I added the following in Startup/Program.cs:

var configurationGoogle = new OpenIdConnectConfiguration()
{
    Issuer = "https://accounts.google.com",
    AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth",
    TokenEndpoint = "https://oauth2.googleapis.com/token",
    UserInfoEndpoint = "https://openidconnect.googleapis.com/v1/userinfo",
    JwksUri = "https://www.googleapis.com/oauth2/v3/certs",
    RegistrationEndpoint = "https://oauth2.googleapis.com/revoke",
};
builder.Services.AddAuthentication(options =>
{
    // options.DefaultSignInScheme = "Cookies";
    // options.DefaultAuthenticateScheme = "Cookies";
    // options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    options.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
}).AddCookie().AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme,
    options =>
    {
        // options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
        // {
        //     RequireExpirationTime = false,
        //     ValidateLifetime = false,
        //     RequireSignedTokens = false,
        //     ValidateIssuerSigningKey = false,
        //     ValidateAudience = false,
        //     ValidateIssuer = false,
        //     RequireAudience = false,
        // };
        options.ClientId = "********.apps.googleusercontent.com";
        options.ClientSecret = "******";
        options.Configuration = configurationGoogle;
        options.SignInScheme = "Cookies";
        options.CallbackPath = new PathString("/signin-google");
        options.Scope.Add("openid");
        options.Scope.Add("email");
        options.Scope.Add("profile");
        options.SaveTokens = true;
        options.ResponseType = OpenIdConnectResponseType.Code;
        options.Events.OnAuthorizationCodeReceived = ctx =>
        {
            Console.WriteLine(@"Code:{0}", ctx.ProtocolMessage.Code);                    
            return Task.CompletedTask;
        };
        options.Events.OnTokenResponseReceived = ctx =>
        {
            Console.WriteLine("I am executing!");
            Console.WriteLine(@"Access Token:{0}", ctx.TokenEndpointResponse.AccessToken);
            List<AuthenticationToken> tokens = ctx.Properties!.GetTokens().ToList();
            tokens.Add(new AuthenticationToken()
            {
                Name = "TicketCreated",
                Value = DateTime.UtcNow.ToString()
            });
            tokens.Add(new AuthenticationToken()
            {
                Name = "access_token",
                Value = ctx.TokenEndpointResponse.AccessToken,
            });

            tokens.Add(new AuthenticationToken()
            {
                Name = "refresh_token",
                Value = ctx.TokenEndpointResponse.RefreshToken,
            });
            ctx!.Properties!.StoreTokens(tokens);
            return Task.CompletedTask;
        };
        options.Events.OnUserInformationReceived = ctx =>
        {
            Console.WriteLine(ctx.User);
            return Task.CompletedTask;
        };
    }
); 

And then try to challange it and after redirection it give me following error:

An unhandled exception occurred while processing the request.
SecurityTokenSignatureKeyNotFoundException: IDX10501: Signature validation failed. Unable to match key:
kid: 'System.String'.
Exceptions caught:
'System.Text.StringBuilder'.
token: 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'.

Microsoft.IdentityModel.Tokens.InternalValidators.ValidateLifetimeAndIssuerAfterSignatureNotValidatedJwt(SecurityToken securityToken, Nullable<DateTime> notBefore, Nullable<DateTime> expires, string kid, TokenValidationParameters validationParameters, StringBuilder exceptionStrings)

As I think, we use code in OIDC and we do not require to verify token (which we have not it but we have access_token here instead!) and only in OAuth 2.0 we should validate it. But it gave me error by the way. Where I am wrong in this procedure here? It should be noticed that I am getting both access_token and refresh_token and also code in OnTokenResponseReceived event and if it not throw error I can do my Authentication Procedure according to Open ID Connect and then persist them with Cookies for next use.How I can prevent this error and Does I am correct regard to OpenIDConnect that we may not verify tokens?!

1

There are 1 answers

0
Mehdi Mowlavi On

The OpenID Connect has an ID Token which requires validation. According to OpenID specification we should validate it using RS256 algorithm which requires a public key from issuer. This public key Id should get from JwksUri for kid specified in ID Token Header. This step do automatically in case of using metadata in OIDC options. But in case of using Configuration as we do in this question, we should specify this keys manually in Option Token Validation Parameters as below:

var GoogleCertsExpected = new JsonWebKeySet();
GoogleCertsExpected.Keys.Add(
     new JsonWebKey
     {
         Alg = "RS256",
         E = "AQAB",
         Kty = "RSA",
         Kid = "58b429662db0786f2efefe13c1eb12a28dc442d0",
         N = "xszAZmDzaUa5d8anZL6ZExj0YNiUVZqzFxWQGKT3fPw5N5lKb_eVtxFKFjgyfOx8Lm1NbVIFVBNTGFsd42MMSU-CrEMMsWe3WTgSzwCmW4t5XE__y1b7MkUTd4WkSzgifMok_SD4D8x-Gd1-awC6nLu0bEbqLWcaXtwfogDiO2nMTgQcuVBGH3ZA-sS7ASgNK-3bNM0mXeVvaIzRPAahZ9tzJ_CEj8mrDVdmgSsO42PTnYtfQc1nytLwNX19_92HQAvWLtQ3-zjZ0FlJUGFTUui8whktgRXv2eXyp-bNkprD7HORUjzCU0Ugwq-nfa1zyYrBDpwQ8FVnS6opUK7iAQ",
         Use = "sig",
     });
GoogleCertsExpected.Keys.Add(
     new JsonWebKey
     {
         Alg = "RS256",
         E = "AQAB",
         Kty = "RSA",
         Kid = "cec13debf4b96479683736205082466c14797bd0",
         N = "1YWUM8Y5UExSfXsBrF6oACI48nITxDf07CiYKn_VTbLRlpXX1AfNtQhrjm-jPjC16qXnGCBhdlZHdCycfezoMg8svo41U7YIVLP5G5H6f7VxAEglmV5IGc0kj35__qmqy3t1Eug_iqxCOyRlcDELQ75MNOhYFQtjeEtLuw4ErpPpOeYVX71vOH3Q9epItMM0n18FXW5Dd6BkCiHvMkb5eSHOH07J0h-MkRF133R-YSPPgDlqLeRxdjDo2rwqKFsOa68edzconVcETWR2YSoFtangVd-IBhzFrax8gyVsntKpmbg8XyJZU2vtgMiTdP0wAjAe8gy78Dg1WIOVOe58lQ",
         Use = "sig",
     });

options.MetadataAddress = "https://accounts.google.com/.well-known/openid-configuration";


options.TokenValidationParameters = new TokenValidationParameters
        {
          IssuerSigningKeys = GoogleCertsExpected.Keys,
        };

We also can do it more dynamicly in option Events as below:

options.Events.OnMessageReceived = async messageReceivedContext =>
    {
        var options = messageReceivedContext.Options;
        if (options.TokenValidationParameters.ValidIssuer == null || options.TokenValidationParameters.IssuerSigningKeys == null)
        {
            var credentials = await options.Backchannel.GetFromJsonAsync<GoogleKeys>(options.Configuration!.JwksUri, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, });
          
            options.TokenValidationParameters.IssuerSigningKeys = credentials.Keys;
            options.TokenValidationParameters.ValidIssuer = options.Authority;
        }
    };

Where GoogleKeys is a class defined as below:

public class GoogleKeys
    {
        public IEnumerable<JsonWebKey> Keys { get; set; }
    }