Google JWT Authentication with AspNet Core 2.0
Asked Answered
E

4

16

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?

Eudemonism answered 11/2, 2018 at 3:7 Comment(10)
I think you need to specify the schema name, check this answerAuerbach
about to modify the question since I seem to have made some progress by adding AddIdentity which apparently specifies the schema.Eudemonism
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?Ratcliffe
@Ratcliffe 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.Eudemonism
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?Ratcliffe
@Ratcliffe 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: developers.google.com/identity/sign-in/web/backend-authEudemonism
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?Eudemonism
It seems that the dotnet google API has a way to validate the tokens: github.com/google/google-api-dotnet-client/pull/1026Eudemonism
Yes that's a reasonable flow, you just need the right components to implement it. Start with JwtBearer and see how far you get.Ratcliffe
Let us continue this discussion in chat.Eudemonism
E
35

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.

Eudemonism answered 13/2, 2018 at 13:41 Comment(10)
is this line correct o.SecurityTokenValidators.Add(new JwtSecurityTokenHandler());? Or did you mean to add your GoogleTokenValidator class?Could
Also I think you should be checking the Audience inside the validate token method to ensure the token is correct.Could
@Could 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?Eudemonism
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.Could
@Could 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.Eudemonism
I am still getting 401 Unauthorized. Can you add all the packages needed?Sycophancy
@Eudemonism This is not working for me with dotnet core 2.1. Can you confirm? Also, unable to find reference to enum AuthenticationTypes.Password.Sycophancy
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;Roborant
@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.Message
validatedToken can't be null otherwise it won't work. After validating the user within google please add this: validatedToken = _tokenHandler.ReadJwtToken(securityToken)Hahn
L
24

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.

Literal answered 26/3, 2018 at 23:35 Comment(13)
I'm confused I have a similar issue . I was trying to use your solution to fix it, but I don't see how you can pass in custom scopesOutwear
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.Literal
@Literal 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!Protonema
@Protonema It doesn't validate access tokens, it validates ID tokens. You need to use GoogleJwtBearerDefaults.AuthenticationScheme instead.Literal
@Literal 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.Fasciation
@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 :)Literal
@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?Simplify
@Simplify 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?Literal
@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 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.Simplify
@Simplify Hmm, but UseGoogle should det the Authority property though? github.com/khellang/Middleware/blob/master/src/…Literal
@khellang, yes I was copying the project code so that I can step into. I must have missed a few lines.Simplify
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?Chevaldefrise
@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 😊Literal
C
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;
            }
        }
    }
}
Composure answered 11/1, 2021 at 1:17 Comment(0)
T
0

after two days of struggling to find a best answer to this , because i wanted to implement it as a backend api which redirects the frontend to the google signin page, it uses jwt authentication as default auth method and use google just for specific method which will work on specific method if i want to.

(Dot Net 7)

so i configured my program.cs like this :

var configuration = builder.Configuration;
//add authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opt =>
{
    opt.SaveToken = true;
    opt.RequireHttpsMetadata = true;
    opt.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidAudience = configuration["JWT:ValidAudience"],
        ValidIssuer = configuration["JWT: ValidIssuer"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Secret"]))
    };
})
.AddGoogle(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];
});

and configure your appsetting like this :

 "Authentication": {
 "Google": {
    "ClientId": "google client id from google",
    "ClientSecret": "google client secret from google",
    "CallbackPath": "/signin-google"
   }
 },
 "Jwt": {
    "ValidAudience": "http://localhost:5157/",
    "ValidIssuer": "http://localhost:5157/",
    "Secret": "your jwt secret"
  }

now all apis requre jwt to authorise also you can authorise with google like this :

    [Authorize(AuthenticationSchemes = GoogleDefaults.AuthenticationScheme)]
    [HttpGet("GoogleAuth")]
    public void Get()
    {   
        // user info from google
        var x = this.User;

        // find the user info in your db if exists
        // user exist // generate jwt token

        // user doesnt exists lets create it
        // auto activate the user
        // generate jwt token


    }   

Also this youtube video can help you

Tetracaine answered 23/11, 2023 at 11:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.