« Back to home

Google Identity Provider with IdentityServer4

Daj się poznać

In this post, I am going to continue my series about IdenityServer4. I will write about forcing IdentityServer to use Google as an external identity token provider. Before I started, I had to register the application that will interact with Google which in my case is IdSrvHost. I am going to reuse the application that I registered in this post. However, I altered the configuration of this application a bit by enabling access to Google+ API. I did this by entering Google APIs and then inside Social APIs I clicked Google+ API. You can see this on this screen:

Enabling Google+ API

Then I simply clicked Enable. And that was it.

Enabling Google+ API

I then added clientId and clientSecret to my secret settings. I wrote about that in this post.

The next step was adding the Google external identity provider to my project with IdentityServer4. I wrote here and here about creating this project and now I have altered it to add an external provider.

The first thing which I did was to add a dependency to project.json and it was called Microsoft.AspNet.Authentication.Google. At the time this post was written, it was in version: 1.0.0-rc1-final.

Then inside Configure method of Startup class I added:

app.UseCookieAuthentication(options =>
    {
        options.AuthenticationScheme = "External";
    });

    app.UseGoogleAuthentication(options =>
    {
        options.AuthenticationScheme = "Google";
        options.SignInScheme = "External";

        options.ClientId = Configuration["GoogleIdentityProvider:ClientId"];
        options.ClientSecret = Configuration["GoogleIdentityProvider:ClientSecret"];
        options.CallbackPath = new PathString("/googlecallback");                
    });

I did this right after the line:

app.UseIdentityServer();

I will explain a bit about what I did here. First of all, I specified CallbackPath in the options for UseGoogleAuthentication but that is not necessary. If you don’t specify the redirect URI, the default will be http://url_of_app/signin-google which I learned when I got the following error:

Redirect URI mismatch

I decided to add something different to see if it would work.

You may be asking: why did I need to add two middlewares? To answer to this question, I must explain that the flow follows the authentication to Google. Now when you login with my Identity Provider (IdSrvHost) you see a screen like this:

Login Screen

This Google button comes from the code in the Login/Index.cshtml view like this:

@if (Model.ExternalProviders.Any())
{
    <div class="col-md-6 col-sm-6 external-providers">
        <div class="panel panel-default">
            <div class="panel-heading">
                <h3 class="panel-title">External Login</h3>
            </div>
            <div class="panel-body">
                <ul class="list-inline">
                    @foreach (var externalProvider in Model.ExternalProviders)
                    {
                        <li>
                            <a class="btn btn-default"
                               href="@Url.Action("ExternalLogin", 
                                    new
                                    {
                                        provider = externalProvider.AuthenticationScheme,
                                        signInId = Model.SignInId
                                    })">
                                @externalProvider.Text
                            </a>
                        </li>
                    }
                </ul>
            </div>
        </div>
    </div>
}

And ExternalProviders in the model I added in the Login method in LoginController like this:

var vm = new LoginViewModel();
vm.ExternalProviders.Add(ExternalProvider.Google);
var loginProviders = HttpContext.Authentication.GetAuthenticationSchemes().ToList();

But then I would have to filter these schemes and choose only the external providers. I would rather be more explicit at this stage of my solution and that’s why I decided to create an explicit list of external providers.

So…what happens when the user clicks the Google button. They see a screen like this:

Allow access

When they click Allow they see a screen like this:

OIDC Client is requesting your permission

Take a notice that this screen comes from my Identity Provider. Next, when the user clicks Yes, Allow they are redirected to the application they came from.

Final result

This is a flow from a GUI perspective. And now flow from the backend perspective.

After clicking Login Only in the JavaScript Oidc Client, there is a redirection to the method Index in the LoginController. Then the Index view is presented. After clicking the Google button, the action ExternalLogin is executed in the same controller and the parameter provider is set to Google. This method looks like this:

public IActionResult ExternalLogin(string provider, string signInId)
{
    var props = new AuthenticationProperties
    {
        RedirectUri = "/login/callback?signInId=" + signInId
    };

    return new ChallengeResult(provider, props);
}

I return ChallengeResult with Google as a provider which caused my Google middleware to be triggered and I also specified the callback URI to be called after the auth process.

The most important thing here is the property SignInScheme which I set in the UseGoogleAuthentication options, as this what completes the authentication process after it returns from Google, and then my callback is called.

In this callback, the current working version looks like this:

public async Task<IActionResult> Callback(string signInId)
{
    var external = await HttpContext.Authentication.AuthenticateAsync("External");
    //todo create or get local account match by email         
    //for now alice is hardcoded            
    var subject = "818727";//todo you get this after you create or get local user
    var name = "alice";
    var claims = new[] {
                new Claim(JwtClaimTypes.Subject, subject),
                new Claim(JwtClaimTypes.Name, name),
                new Claim(JwtClaimTypes.IdentityProvider, "idsvr"),
                new Claim(JwtClaimTypes.AuthenticationTime, DateTime.UtcNow.ToEpochTime().ToString())
            };

    var ci = new ClaimsIdentity(claims, "password", JwtClaimTypes.Name, JwtClaimTypes.Role);
    var cp = new ClaimsPrincipal(ci);

    await HttpContext.Authentication.SignInAsync(Constants.PrimaryAuthenticationType, cp);
    await HttpContext.Authentication.SignOutAsync("External");

    if (signInId != null)
    {                
        return new SignInResult(signInId);
    }

    return Redirect("~/");
}

I can get access to the claims received from Google with this line:

var external = await HttpContext.Authentication.AuthenticateAsync("External");

That’s why I needed this extra middleware to get these Google claims. I should do something meaningful with these claims but that is a theme for another post. For now, I will simply map everyone logged in with a Google account to my local user alice. Next, I signed out from the External authentication schema, then signed in alice with PrimaryAuthenticationType which is IdentityServer. After, I return the SignInResult. Here, I did here the same thing done in the post Login method in LoginController. Basically, IdentityServer is doing its job and I don’t need to micromanage what’s going on there. I provided everything it needs to authenticate and authorize a user.

And that’s it…this is how I setup Google as an external Identity Provider. You can find the whole source code for this sample project in this repository. Similarly, for example, you can use Facebook as an external Identity Provider. I am going to do this but I will write about it only if I run into problems and I’m forced to troubleshoot.

Related posts:

Comments

comments powered by Disqus