Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IdentityServer4 - missing claims from Google

TLDR; In the context of using IdentityServer4

  1. How do you get email address and hd claims from Google?
  2. How do you get User.Identity.Name to be populated?

I have worked through the IdentityServer quickstarts and have a working MVC client talking to a IdentityServer instance (apologies if using the wrong terminology). I am using External Authentication (Google) and do not have anything mildly complicated such as local logins / database etc. I am not using ASP.NET Identity. This is all working just fine.

I can successfully authenticate in my MVC app and the following code produces the claims in the screenshot below:

@foreach (var claim in User.Claims)
{
    <dt>@claim.Type</dt>
    <dd>@claim.Value</dd>
}
<dt>Identity.Name</dt>
<dd>&nbsp;@User.Identity.Name</dd>

<dt>IsAuthenticated</dt>
<dd>@User.Identity.IsAuthenticated</dd>

enter image description here

Questions:

  1. I cannot retrieve extra claims (right term?) from Google. Specifically 'hd' or even 'email' - note that they don't show up in the claims in the above screenshot. How do I get the email address and hd claims from Google? What am I missing or doing wrong?
  2. Note that the output of User.Identity.Name is empty. Why is this and how do I get this populated? This seems to be the only property of User.Identity that isn't set.

My setup is as follows - you can see the output of this as above:

Client (MVC)

In Startup.cs, ConfigureServices

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
    options.SignInScheme = "Cookies";
    options.Authority = Configuration["App:Urls:IdentityServer"];
    options.RequireHttpsMetadata = false;
    options.Resource = "openid profile email";
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.Scope.Add("domain");
    options.ClientId = "ctda-web";
    options.SaveTokens = true;
    options.GetClaimsFromUserInfoEndpoint = true;
});

Identity Server

Client definition

// OpenID Connect implicit flow client (MVC)
new Client
{
    ClientId = "ctda-web",
    ClientName = "Company To Do Web App",
    AllowedGrantTypes = GrantTypes.Implicit,
    EnableLocalLogin = false,

    // where to redirect to after login
    RedirectUris = { "http://localhost:53996/signin-oidc" },

    // where to redirect to after logout
    PostLogoutRedirectUris = { "http://localhost:53996/signout-callback-oidc" },

    AllowedScopes = new List<string>
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        IdentityServerConstants.StandardScopes.Email,
        "domain"
    }
}

IdentityResource definition

return new List<IdentityResource>
{
    new IdentityResources.OpenId(),
    new IdentityResources.Profile(),
    new IdentityResources.Email(),
    new IdentityResource
    {
        Name = "domain",
        DisplayName = "Google Organisation",
        Description = "The hosted G Suite domain of the user, if part of one",
        UserClaims = new List<string> { "hd"}
    } 
};
like image 551
hanzworld Avatar asked Dec 18 '22 03:12

hanzworld


1 Answers

The answer is amazingly unobvious: the sample code provided by IdentityServer4 works as long as you have the following configuration (in the Identity Server Startup.cs):

services.AddIdentityServer()
    .AddDeveloperSigningCredential()
    .AddInMemoryApiResources(Config.GetApiResources())
    .AddInMemoryIdentityResources(Config.GetIdentityResources())
    .AddInMemoryClients(Config.GetClients()
    .AddTestUsers(Config.GetUsers()); //<--- this line here

Why? Because AddTestUsers is doing a bunch of the plumbing you need to do in your own world. The walkthrough implicitly assume you move to EF or ASP.NET Identity etc and make it unclear what you have to do if you aren't going to use these data stores. In short, you need to:

  1. Create an object to represent the user (here's a starter)
  2. Create a persistance/query class(here's a starter)
  3. Create an instance of IProfileService which ties this all together, putting in your definitions of a User and a UserStore (here's a starter)
  4. Add appropriate bindings etc

My IdentityServer Startup.cs ended up looking like this (I want to do in memory deliberately, but obviously not use the test users provided in the samples):

services.AddSingleton(new InMemoryUserStore()); //<-- new

services.AddIdentityServer()
    .AddDeveloperSigningCredential()
    .AddInMemoryApiResources(Config.GetApiResources())
    .AddInMemoryIdentityResources(Config.GetIdentityResources())
    .AddInMemoryClients(Config.GetClients())
    .AddProfileService<UserProfileService>(); //<-- new

Turns out Google does return email as part of the claim. The scopes in the code sample of the question worked.

like image 80
hanzworld Avatar answered Dec 28 '22 06:12

hanzworld