Tag Archives: OpenId Connect

Google+ shutdown: fixing Google authentication in ASP.NET Core

A few months ago, Google decided to shutdown Google+, due to multiple data leaks. More recently, they announced that the Google+ APIs will be shutdown on March 7, 2019, which is pretty soon! In fact, calls to these APIs might start to fail as soon as January 28, which is less than 3 weeks from now. You might think that it doesn’t affect you as a developer; but if you’re using Google authentication in an ASP.NET Core app, think again! The built-in Google authentication provider (services.AddAuthentication().AddGoogle(...)) uses a Google+ API to retrieve information about the signed-in user, which will soon stop working. You can read the details in this Github thread. Note that it also affects classic ASP.NET MVC.

OK, now I’m listening. How do I fix it?

Fortunately, it’s not too difficult to fix. There’s already a pull request to fix it in ASP.NET Core, and hopefully an update will be released soon. In the meantime, you can either:

  • use the workaround described here, which basically specifies a different user information endpoint and adjusts the JSON mappings.
  • or use the generic OpenID Connect authentication provider instead, which I think is better than the built-in provider anyway, because you can get all the necessary information directly from the ID token, without making an extra API call.

Using OpenID Connect to authenticate with Google

So, let’s see how to change our app to use the OpenID Connect provider instead of the built-in Google provider, and configure it to get the same results as before.

First, let’s install the Microsoft.AspNetCore.Authentication.OpenIdConnect NuGet package to the project, if it’s not already there.

Then, we go to the place where we add the built-in Google provider (the call to AddGoogle, usually in the Startup class), and remove that call.

Instead, we add the OpenID Connect provider, point it to the Google OpenID Connect authority URL, and set the client id (the same that we were using for the built-in Google provider):

services
    .AddAuthentication()
    .AddOpenIdConnect(
        authenticationScheme: "Google",
        displayName: "Google",
        options =>
        {
            options.Authority = "https://accounts.google.com/";
            options.ClientId = configuration["Authentication:Google:ClientId"];
        });

We also need to adjust the callback path to be the same as before, so that the redirect URI configured for the Google app still works; and while we’re at it, let’s also configure the signout paths.

options.CallbackPath = "/signin-google";
options.SignedOutCallbackPath = "/signout-callback-google";
options.RemoteSignOutPath = "/signout-google";

The default configuration already includes the openid and profile scopes, but if we want to have access to the user’s email address as we did before, we also need to add the email scope:

options.Scope.Add("email");

And that’s it! Everything should work as it did before. Here’s a Gist that shows the code before and after the change.

Hey, where’s the client secret?

You might have noticed that we didn’t specify the client secret. Why is this?

The built-in Google provider is actually just a generic OAuth provider with Google-specific configuration. It uses the authorization code flow, which requires the client secret to exchange the authorization code for an access token, which in turn is used to call the user information endpoint.

But by default the OpenId Connect provider uses the implicit flow. There isn’t an authorization code: an id_token is provided directly to the redirect_uri, and there’s no need to call any API, so no secret is needed. If, for some reason, you don’t want to use the implicit flow, just change options.ResponseType to code (the default is id_token), and set options.ClientSecret as appropriate. You should also set options.GetClaimsFromUserInfoEndpoint to true to get the user details (name, email…), since you won’t have an id_token to get them from.

Multitenant Azure AD issuer validation in ASP.NET Core

If you use Azure AD authentication and want to allow users from any tenant to connect to your ASP.NET Core application, you need to configure the Azure AD app as multi-tenant, and use a "wildcard" tenant id such as organizations or common in the authority URL:

openIdConnectOptions.Authority = "https://login.microsoftonline.com/organizations/v2.0";

The problem when you do that is that with the default configuration, the token validation will fail because the issuer in the token won’t match the issuer specified in the OpenID metadata. This is because the issuer from the metadata includes a placeholder for the tenant id:

https://login.microsoftonline.com/{tenantid}/v2.0

But the iss claim in the token contains the URL for the actual tenant, e.g.:

https://login.microsoftonline.com/64c5f641-7e94-4d21-ae5c-9747994e4211/v2.0

A workaround that is often suggested is to disable issuer validation in the token validation parameters:

openIdConnectOptions.TokenValidationParameters.ValidateIssuer = false;

However, if you do that the issuer won’t be validated at all. Admittedly, it’s not much of a problem, since the token signature will prove the issuer identity anyway, but it still bothers me…

Fortunately, you can control how the issuer is validated, by specifying the TokenValidator property:

options.TokenValidationParameters.IssuerValidator = ValidateIssuerWithPlaceholder;

Where ValidateIssuerWithPlaceholder is the method that validates the issuer. In that method, we need to check if the issuer from the token matches the issuer with a placeholder from the metadata. To do this, we just replace the {tenantid} placeholder with the value of the token’s tid claim (which contains the tenant id), and check that the result matches the token’s issuer:

private static string ValidateIssuerWithPlaceholder(string issuer, SecurityToken token, TokenValidationParameters parameters)
{
    // Accepts any issuer of the form "https://login.microsoftonline.com/{tenantid}/v2.0",
    // where tenantid is the tid from the token.

    if (token is JwtSecurityToken jwt)
    {
        if (jwt.Payload.TryGetValue("tid", out var value) &&
            value is string tokenTenantId)
        {
            var validIssuers = (parameters.ValidIssuers ?? Enumerable.Empty<string>())
                .Append(parameters.ValidIssuer)
                .Where(i => !string.IsNullOrEmpty(i));

            if (validIssuers.Any(i => i.Replace("{tenantid}", tokenTenantId) == issuer))
                return issuer;
        }
    }

    // Recreate the exception that is thrown by default
    // when issuer validation fails
    var validIssuer = parameters.ValidIssuer ?? "null";
    var validIssuers = parameters.ValidIssuers == null
        ? "null"
        : !parameters.ValidIssuers.Any()
            ? "empty"
            : string.Join(", ", parameters.ValidIssuers);
    string errorMessage = FormattableString.Invariant(
        $"IDX10205: Issuer validation failed. Issuer: '{issuer}'. Did not match: validationParameters.ValidIssuer: '{validIssuer}' or validationParameters.ValidIssuers: '{validIssuers}'.");

    throw new SecurityTokenInvalidIssuerException(errorMessage)
    {
        InvalidIssuer = issuer
    };
}

With this in place, you’re now able to fully validate tokens from any Azure AD tenant without skipping issuer validation.

Happy coding, and merry Christmas!