15

I am trying to integrate google authentication in my ASP.NET Core 2.0 web api and I cannot figure out how to get it to work.

I have this code in my Startup.cs ConfigureServices:

services.AddIdentity<ApplicationUser, IdentityRole>()
.AddDefaultTokenProviders();

services.AddAuthentication()
.AddGoogle(googleOptions => 
 {
     googleOptions.ClientId = Configuration["Authentication:Google:ClientId"];
     googleOptions.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
});

And this in Configure(IApplicationBuilder app, IHostingEnvironment env):

 app.UseAuthentication();

When I navigate to an Authorized endpoint, the result is a 302 Found because presumably it is redirecting to some login endpoint (which I never created). How do I prevent the redirection and just have the API expect a token and return a 401 if no token is provided?

Mikeyg36
  • 2,718
  • 4
  • 24
  • 25
  • I think you need to specify the schema name, check this [answer](https://stackoverflow.com/a/46285807/1025936) – Abiezer Feb 11 '18 at 03:17
  • about to modify the question since I seem to have made some progress by adding AddIdentity which apparently specifies the schema. – Mikeyg36 Feb 11 '18 at 03:18
  • 1
    This Google provider doesn't handle API tokens. That's what JwtBearer is for. Or were you trying to use it side by side with tokens? – Tratcher Feb 11 '18 at 03:28
  • @Tratcher I am building a backend for a mobile client (react native). I suppose then all I want is the ability for my API to handle a google token that is provided from my react native front-end. Does that workflow make sense? And is the google middleware I am using the wrong way to do that? I am new to Auth in general. – Mikeyg36 Feb 11 '18 at 04:38
  • 1
    The google auth handler is built for interactive browser apps, not API access. If the token from google is a JWT then you can process it with the JwtBearer handler, that uses the 401 auth flow you're asking for. If it's not a JWT then life gets more interesting. Even if the client sends you a token how do you plan to validate it? Most tokens are opaque, you can only confirm they're valid by using them to make a call to a Google API. Were you planning on calling those APIs anyways? – Tratcher Feb 11 '18 at 04:44
  • @Tratcher My thought was I could have the client authenticate with google, which would generate a JWT token, and then they could pass that token to my backend, the backend could validate that the token is legit, and then authorize the user using that google identity. This seems to be the workflow described by this doc: https://developers.google.com/identity/sign-in/web/backend-auth – Mikeyg36 Feb 11 '18 at 04:49
  • As I said, I am new to Auth. I just assumed this was a standard way to do things? I see "Sign in with google" all over the web and mobile apps. Is this not the approach these apps take? – Mikeyg36 Feb 11 '18 at 04:52
  • It seems that the dotnet google API has a way to validate the tokens: https://github.com/google/google-api-dotnet-client/pull/1026 – Mikeyg36 Feb 11 '18 at 05:12
  • Yes that's a reasonable flow, you just need the right components to implement it. Start with JwtBearer and see how far you get. – Tratcher Feb 11 '18 at 07:11
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/164921/discussion-between-mikeyg36-and-tratcher). – Mikeyg36 Feb 11 '18 at 15:12

3 Answers3

34

Posting my ultimate approach for posterity.

As Tratcher pointed out, the AddGoogle middleware is not actually for a JWT authentication flow. After doing more research, I realized that what I ultimately wanted is what is described here: https://developers.google.com/identity/sign-in/web/backend-auth

So my next problems were

  1. I could not rely on the standard dotnet core Jwt auth middleware anymore since I need to delegate the google token validation to google libraries
  2. There was no C# google validator listed as one of the external client libraries on that page.

After more digging, I found this that JWT validation support was added to C# here using this class and method: Google.Apis.Auth.Task<GoogleJsonWebSignature.Payload> ValidateAsync(string jwt, GoogleJsonWebSignature.ValidationSettings validationSettings)

Next I needed to figure out how to replace the built in JWT validation. From this SO questions I came up with an approach: ASP.NET Core JWT Bearer Token Custom Validation

Here is my custom GoogleTokenValidator:

public class GoogleTokenValidator : ISecurityTokenValidator
{
    private readonly JwtSecurityTokenHandler _tokenHandler;

    public GoogleTokenValidator()
    {
        _tokenHandler = new JwtSecurityTokenHandler();
    }

    public bool CanValidateToken => true;

    public int MaximumTokenSizeInBytes { get; set; } = TokenValidationParameters.DefaultMaximumTokenSizeInBytes;

    public bool CanReadToken(string securityToken)
    {
        return _tokenHandler.CanReadToken(securityToken);
    }

    public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
    {
        validatedToken = null;
        var payload = GoogleJsonWebSignature.ValidateAsync(securityToken, new GoogleJsonWebSignature.ValidationSettings()).Result; // here is where I delegate to Google to validate

        var claims = new List<Claim>
                {
                    new Claim(ClaimTypes.NameIdentifier, payload.Name),
                    new Claim(ClaimTypes.Name, payload.Name),
                    new Claim(JwtRegisteredClaimNames.FamilyName, payload.FamilyName),
                    new Claim(JwtRegisteredClaimNames.GivenName, payload.GivenName),
                    new Claim(JwtRegisteredClaimNames.Email, payload.Email),
                    new Claim(JwtRegisteredClaimNames.Sub, payload.Subject),
                    new Claim(JwtRegisteredClaimNames.Iss, payload.Issuer),
                };

        try
        {
            var principle = new ClaimsPrincipal();
            principle.AddIdentity(new ClaimsIdentity(claims, AuthenticationTypes.Password));
            return principle;
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;

        }
    }
}

And in Startup.cs, I also needed to clear out the default JWT validation, and add my custom one:

services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

            })
            .AddJwtBearer(o =>
                {
                    o.SecurityTokenValidators.Clear();
                    o.SecurityTokenValidators.Add(new GoogleTokenValidator());
                }

Maybe there is an easier way, but this is where I landed and it seems to work fine! There was additional work I did that I left out of here for simplicity, for example, checking if there is already a user in my user's DB that matches the claims provided by google, so I apologize if the code above does not 100% work since I may have removed something inadvertently.

Mikeyg36
  • 2,718
  • 4
  • 24
  • 25
  • 1
    is this line correct `o.SecurityTokenValidators.Add(new JwtSecurityTokenHandler());`? Or did you mean to add your `GoogleTokenValidator` class? – CodePB Feb 17 '18 at 15:01
  • Also I think you should be checking the Audience inside the validate token method to ensure the token is correct. – CodePB Feb 17 '18 at 16:11
  • @CodePB you are correct that I should have written `new GoogleTokenValidator()`, thanks! Regarding the Aud claim, can you clarify why you think I need to validate that? `GoogleJsonWebSignature.ValidateAsync` takes care of all the token validation using a public google cert. Am I missing something? – Mikeyg36 Feb 18 '18 at 18:27
  • 5
    You validate it is a valid token, but not one meant for your site. You need to pass the audience (the clientId) you set up in google to provide an extra level of security. You can do this by adding to `new GoogleJsonWebSignature.ValidationSettings()` and set the property `Audience = "YourClientId"`. If you don't set this, the google validation skips Audience validation to the best of my knowledge. – CodePB Feb 19 '18 at 19:56
  • 1
    @CodePB I see what you are saying. I just finished watching a Pluralsight course that explains OAuth2 and OpenID Connect, so everything makes a lot more sense now. Thanks for the feedback! I will update the answer when I get around to updating my code. – Mikeyg36 Feb 25 '18 at 02:41
  • I am still getting 401 Unauthorized. Can you add all the packages needed? – Manish Jain Jun 19 '18 at 00:22
  • 1
    @Mikeyg36 This is not working for me with dotnet core 2.1. Can you confirm? Also, unable to find reference to enum AuthenticationTypes.Password. – Manish Jain Jun 21 '18 at 06:42
  • 2
    Usings needed for this answer: using Google.Apis.Auth; using System; using System.Collections.Generic; using System.Security.Claims; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; – lsp Sep 11 '19 at 19:52
  • @ManishJain I was getting the same, my error was that I was making the request setting the Authorization header, but did not put Bearer infront of the actual token. – vsarunov Nov 23 '19 at 09:34
  • validatedToken can't be null otherwise it won't work. After validating the user within google please add this: validatedToken = _tokenHandler.ReadJwtToken(securityToken) – José Salgado Mar 23 '22 at 10:03
22

I just published a NuGet package to handle validation of Google OpenID Connect tokens.

The package relies on Microsoft's JWT validation and authentication handler from Microsoft.AspNetCore.Authentication.JwtBearer, with some added validation around hosted domains.

It contains a single public extension method, UseGoogle, on JwtBearerOptions that lets you configure the handler to validate Google OpenID Connect tokens, without other dependencies:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(jwt => jwt.UseGoogle(
        clientId: "<client-id-from-Google-API-console>",
        hostedDomain: "<optional-hosted-domain>"));

If you want to take a look at the source, you can find it here.

khellang
  • 17,550
  • 6
  • 64
  • 84
  • I'm confused I have a [similar issue](https://stackoverflow.com/questions/54451807/use-jwt-with-oauth-authentication-in-net-core) . I was trying to use your solution to fix it, but I don't see how you can pass in custom scopes – johnny 5 Jan 31 '19 at 01:33
  • Scopes are specified when requesting a JWT. This is for validating an already issued JWT. You should specify the scopes you need when requesting the JWT from Google, not in here. – khellang Jan 31 '19 at 11:07
  • @khellang If we add this to our Auth chain, does it then allow us to validate the Google AccessToken using something like this? var result = await HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);? If so, are we still using Cookies Auth with Google? Thanks! – Dumber_Texan2 Sep 04 '19 at 21:01
  • @Dumber_Texan2 It doesn't validate access tokens, it validates ID tokens. You need to use `GoogleJwtBearerDefaults.AuthenticationScheme` instead. – khellang Sep 05 '19 at 08:45
  • @khellang is `UseGoogle` coming from another Nuget package? It's not recognized. I have the package you mentioned above and Microsoft.AspNetCore.Authentication.Google as well. – dotnetspark May 11 '20 at 12:38
  • @yopez83 The `UseGoogle` extension method comes from the `Hellang.Authentication.JwtBearer.Google` NuGet package. The `Microsoft.AspNetCore.Authentication.Google` package is for doing back-end authentication against Google. If your client (typically a SPA) already has a JWT token, you can just use the code posted above - no need for the additional package :) – khellang May 11 '20 at 20:22
  • @khellang, the validation failed when calling base.ValidateToken and threw exception "IDX10501: Signature validation failed. Unable to match key: kid: System.String". So I still need to use the google GoogleJsonWebSignature class to validate. Is it a known issue? – Hengyi Oct 31 '21 at 17:17
  • @Hengyi Not a known issue. I've been successfully using the NuGet package in production since I posted the answer. What are your settings? Maybe you can file an issue with your setup code so we could have a look at the details? – khellang Nov 01 '21 at 18:21
  • @Khellang, I realized my error. When I tested the code, I forgot to set Authority property in JwtBearerOptions. Internally it uses Authority url to call https://accounts.google.com/.well-known/openid-configuration to get the signing keys. So without authority, it was not able to validate the signature. It works now. Thanks. – Hengyi Nov 07 '21 at 03:10
  • @Hengyi Hmm, but `UseGoogle` should det the `Authority` property though? https://github.com/khellang/Middleware/blob/master/src/Authentication.JwtBearer.Google/JwtBearerOptionsExtensions.cs#L45 – khellang Nov 07 '21 at 08:34
  • @khellang, yes I was copying the project code so that I can step into. I must have missed a few lines. – Hengyi Nov 09 '21 at 05:23
  • Great lib @khellang! I understand the issuer key (clientSecret) is not needed as the Token is using RSA. What is the clientSecret issued by google good for? – Igor Lankin Dec 04 '21 at 22:44
  • 1
    @IgorLankin There's no need for a client secret in this library as it's simply validating existing tokens issued by Google. Secrets are passed to Google by confidential (as opposed to public) clients to request tokens – khellang Dec 05 '21 at 23:17
0

Mikeyg36's answer was terrific and finally helped me sort out my jwt token issues. However, I added the clientId which I feel is important since you don't want to validate any id token that comes in. I also added "JwtBearerDefaults.AuthenticationScheme" to the AddIdentity.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Google.Apis.Auth;

namespace Some.Namespace
{
    public class GoogleTokenValidator : ISecurityTokenValidator
    {
        private readonly string _clientId;
        private readonly JwtSecurityTokenHandler _tokenHandler;

        public GoogleTokenValidator(string clientId)
        {
            _clientId = clientId;
            _tokenHandler = new JwtSecurityTokenHandler();
        }

        public bool CanValidateToken => true;

        public int MaximumTokenSizeInBytes { get; set; } = TokenValidationParameters.DefaultMaximumTokenSizeInBytes;

        public bool CanReadToken(string securityToken)
        {
            return _tokenHandler.CanReadToken(securityToken);
        }

        public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
        {
            validatedToken = null;
            try {
                var payload = GoogleJsonWebSignature.ValidateAsync(securityToken, new GoogleJsonWebSignature.ValidationSettings() { Audience =  new[] { _clientId }}).Result; // here is where I delegate to Google to validate
            
                var claims = new List<Claim>
                    {
                        new Claim(ClaimTypes.NameIdentifier, payload.Name),
                        new Claim(ClaimTypes.Name, payload.Name),
                        new Claim(JwtRegisteredClaimNames.FamilyName, payload.FamilyName),
                        new Claim(JwtRegisteredClaimNames.GivenName, payload.GivenName),
                        new Claim(JwtRegisteredClaimNames.Email, payload.Email),
                        new Claim(JwtRegisteredClaimNames.Sub, payload.Subject),
                        new Claim(JwtRegisteredClaimNames.Iss, payload.Issuer),
                    };

                var principle = new ClaimsPrincipal();
                principle.AddIdentity(new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme));
                return principle;
            }
            catch (Exception e)
            {
                Debug.WriteLine(e);
                throw;
            }
        }
    }
}
vidalsasoon
  • 4,365
  • 1
  • 32
  • 40