Get name and email claim using IdentityServer from Google auth (SPA) - c#

I'm new to using Identity Server for SPA auth but I started following this example: Authentication and authorization for SPAs and with some tinkering I've now also added Google auth. However, I'm having trouble getting the external Google claims merged into my application's claims (for example: given_name).
I've verified that Google does send back the appropriate claims but nothing seems to map those claims, e.g. options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");. When I access one of my protected endpoints my claims do not include any of the additional google claims.
I did find some additional documentation Persist additional claims... which tells me to add the claim in OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) but since this is an SPA that page doesn't exist. Is there another approach to this? I haven't been able to find much that doesn't use the Page / OnPostConfirmationAsync.
Thanks
Including relevant details from my Startup.cs in case I'm doing something wrong here:
I've tried a few different variants from other examples I've found but
services
.AddDefaultIdentity<AppUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<AppRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddIdentityServer()
.AddApiAuthorization<AppUser, ApplicationDbContext>();
services
.AddAuthentication()
.AddIdentityServerJwt()
.AddGoogle(options =>
{
options.ClientId = Configuration["Auth:Google:ClientId"];
options.ClientSecret = Configuration["Auth:Google:ClientSecret"];
options.AuthorizationEndpoint += "?prompt=consent"; // Hack so we always get a refresh token, it only comes on the first authorization response
options.AccessType = "offline";
options.SaveTokens = true;
options.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
options.Scope.Add("https://www.googleapis.com/auth/userinfo.profile");
options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
})
And my api is simply:
[Authorize()]
[Route("test")]
public IActionResult Test()
{
var all = User.Claims.Select(s => $"{s.Type}: {s.Value}");
return Ok(all);
}

The only way I've been able to handle this is to scaffold the needed ExternalLogin page (like #d_f mentioned in the question's comments) and then continue following the Persist additional claims steps. I was hoping I could just set a collection of claims to keep in my Startup file and it would work but Identity Server just uses its internal ExternalLogin (Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal) and the code there doesn't handle adding external claims. This works for now but I would prefer a way of not needing to scaffold the ExternalLogin page.
I tried asking for clarification on the official docs but my question was closed (instead they suggested I ask on StackOverflow - lol). However there seems to be some work being done on improving the docs and flowing the claims through, if interested you can dig through this GitHub issue: https://github.com/dotnet/AspNetCore.Docs/issues/16488

Related

Where in the API is the value of ApiName for authorization with IdentiyServer4 being assigned?

According to the official docs, in order to access API on a controller withing the same project as the identity provider, I'm supposed to have an equivalent to the following lines, as exemplified at the official site.
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://demo.identityserver.io";
options.ApiName = "api1";
});
}
It doesn't work in my project (I get 401 despite following this answer), so I removed the option.ApiName=... altogether, only keeping the authority setting. Now it works but it confuses me now.
Now, where is that api1 supposed to be set?
Since I'm apparently not setting it, why does the server let me in?
To me, it appears like this.
With ApiName set.
-"Password!"
-"Hmmm... 'HakunaMatata'..."?
-"Wrong! GFY!"
Without ApiName set.
-"Password!"
-"Hmmm..." [wall of silence]?
-"Ah, well. You may pass."
-"Hehe, you can GFY..."
For IdentityServer4 and .NET Core you should not use AddIdentityServerAuthentication, but instead use the AddJwtBearer. The documentation you link to is for version 3 of IdentityServer.
See this link
If the ApiName is set, then it is set as the audience and the audience is validated, see how it is handled in the code here. If it is not set, then only the scopes are validated.

Multiple Authentication Schemes ASPNET Core 3

See Update below
I'm using Azure AD B2C and I'd like my users to be able to log in thru my web app as well as be able to utilize JWT bearer tokens and call Web API methods from a mobile app.
I can get either authentication scheme to work by itself. For example, in my startup.cs I can do the following:
services
.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options));
which works as expected (a user can login on the web site, but JWT doesn't work).
Alternatively, I can instead use the following and then only JWT bearer tokens will work:
services
.AddAuthentication(AzureADB2CDefaults.JwtBearerAuthenticationScheme)
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
If I want either to work, I can do the following (with the help of https://stackoverflow.com/a/49706390)
services
.AddAuthentication()
.AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options))
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(AzureADB2CDefaults.AuthenticationScheme, AzureADB2CDefaults.JwtBearerAuthenticationScheme)
.Build();
}
And now either will work. (edit: actually, they don't work completely)
HOWEVER, I also have this code:
app.UseAuthentication();
app.UseMiddleware<MyAfterAuthenticatedMiddleware>();
app.UseAuthorization();
The problem is that when I use the combination of either authentication, when my middleware code runs, my user is not authenticated (in the middleware code) and has no claims, etc. but obtains them later in the pipeline.
What's happening here? And how can this be fixed?
It seems that when I don't specify a default authentication scheme--in order to have multiple schemes--the authentication is not happening until the authorization step in the pipeline.
I need my middleware to run after authentication and before authorization.
How can I make that happen with the multiple authentication schemes?
UPDATE -- Solved! But there must be a better way!??
First of all, to the people who have created the .NET security stuff, I say kudos. It's important and it's difficult. However I do think there may be a lot of room for improvement.
Most developers dabble in security when they have to, and then go back to their "regular" job". Unless you work with it every day, it's tough to keep on top of. Every time you go back to it, everything's changed yet again.
It must be a common scenario: I want my users to be able to log in to my web site and interact with various web API methods. I would like them to also be able to access those same API methods via another means, such as a mobile app--where I'd be using JWT tokens, or equivalent.
This shouldn't be hard to make work.
However, I was tying myself into knots creating handlers for this and policies for that. One thing would work, but another thing wouldn't. Then when I thought things were right--I discovered some of the challenge and forbid logic didn't work as expected.
The built-in Authorization middleware has the ability to do authentication -- this was one of the early roads I went down, only to discover that it didn't fully work -- and it caused other problems for me, as described above.
In my opinion, authentication should not happen during authorization. Authentication should happen where it is expected--in authentication middleware. (My guess is that it was added in authorization in order to work around some other problem that presented itself years ago -- and perhaps still exists today)
Anyway--here is how I finally got things to work. It could be a lot cleaner and slicker and more flexible, but it works for my needs. And it is less of a hack than anything else I have seen. But is there a nicer, built-in class that could have done this for me?
My new question is this: is there a better way to get this done than how I've done it as described below?. It's hard to believe this is the best way.
In Startup.ConfigureServices I have now have the following:
services
.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options))
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
I then also have:
services.AddHttpContextAccessor();
services.AddSingleton<IAuthenticationSchemeProvider, MyAuthenticationSchemeProvider>();
And finally, I have a new class:
public class MyAuthenticationSchemeProvider : AuthenticationSchemeProvider
{
public MyAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IHttpContextAccessor httpContextAccessor) : base(options)
{
HttpContextAccessor = httpContextAccessor;
}
protected MyAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes, IHttpContextAccessor httpContextAccessor) : base(options, schemes)
{
HttpContextAccessor = httpContextAccessor;
}
private IHttpContextAccessor HttpContextAccessor { get; }
private bool IsBearerRequest()
{
var httpContext = HttpContextAccessor.HttpContext;
return httpContext.Request.Headers.ContainsKey("Authorization")
&& httpContext.Request.Headers["Authorization"].Any(x => x.ToLower().Contains("bearer"));
}
public async Task<AuthenticationScheme> GetMySchemeAsync()
{
return IsBearerRequest()
? await GetSchemeAsync(AzureADB2CDefaults.BearerAuthenticationScheme)
: await base.GetDefaultAuthenticateSchemeAsync();
}
public override async Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
{
return await GetMySchemeAsync();
}
public override Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync()
{
return GetMySchemeAsync();
}
public override Task<AuthenticationScheme> GetDefaultForbidSchemeAsync()
{
return GetMySchemeAsync();
}
}
Now I can use both kinds of authentication, the challenge & forbid work as expected. Why isn't there a built-in class that allows for switching between authentication schemes? Why does the authorization middleware attempt to authenticate with multiple schemes (I say it shouldn't do it at all), but not the authentication middleware?
Now this is here for anyone else who struggles with a similar issue.

Multiple Google Authentication scopes in .NET Core depending on controller

I have a web based app. This app allows users to sign up/in using Google Auth as per this code in Startup.cs
services.AddAuthentication().AddGoogle(googleOptions =>
{
googleOptions.ClientId = Configuration["ClientId"];
googleOptions.ClientSecret = Configuration["CliSecret"];
...
});
This all works nicely with the out-of-the-box Identity system so I can register users.
However, I also want users to be able to 'connect' to other Google services with separate accounts after the sign up in a separate area of the site.
For example, I might want a user to connect their AdWords account.
They will authenticate with Google via a non-Identity flow and the relevant info (token, refresh token etc) will be stored independantly in the db (i.e it won't store a 'User' in the AspNetUSers table).
Can I change the authentication scope in the controller before I make my initial call to google?
It'd be nice to utilize the same Authentication service but with some extra scope in this case. Is that possible?
Alternatively, have another Google section in Startup.cs...maybe like:
services.AddAuthentication().AddGoogle(googleOptions =>
{
googleOptions.ClientId = Configuration["ClientId"];
googleOptions.ClientSecret = Configuration["CliSecret"];
googleOptions.Scope.Add("https://www.googleapis.com/auth/adwords"); //*** THIS IS THE EXTRA SCOPE NEEDED ***
...
});
We had similar problem, our Identity Provider should be able to login users of defferent clients with different Google account
We decided to add multiple Google areas as you suggested. The main point here is that each area (which defines some google account) uses unique cookie scheme.
When we create login URL, we get google account needed for that client, get it's cookie scheme and create correct URL for Google Authenticate button
code example:
public static class AuthenticationBuilderGoogleAdder
{
public static AuthenticationBuilder AddGoogleAuth(this AuthenticationBuilder authenticationBuilder, IServiceCollection services)
{
var serviceProvider = services.BuildServiceProvider();
// create IThirdPartyProvidersProvider realization with GetByProviderCode method
var authThirdPartyProvidersProvider = serviceProvider.GetService<IThirdPartyProvidersProvider>();
var googleProviders = authThirdPartyProvidersProvider.GetByProviderCode("google");
googleProviders.ForEach(p =>
{
authenticationBuilder = authenticationBuilder.AddGoogle(p.CookieScheme, options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = p.ClientId;
options.ClientSecret = p.ClientSecret;
});
});
return authenticationBuilder;
}
}
register it as
services.AddAuthentication()
.AddGoogleAuth(services)
We call services.BuildServiceProvider() in order to create another container with services which were already registered in DI, in order to get Google accounts with different cookie schemas from the DB

How to use IdentityServer4 Access Token and [Authorize] attribute

I have the solution with projects: IdentityServer4, ApiServer, MvcClient. I use Hybrid flow. Auth works very well but I can't get the role in MvcClient.
In the MvсСlient app, after authorization, I get access_token. The token contains the necessary claims. But the MVC application cannot access to the user role.
That is, it is assumed that I will call the external API from the MVC application. But I also need the MVC application to be able to use the user role.
Attribute [Authorize] works fine but [Authorize(Roles =
"admin")] doesn't work!
Source code here: gitlab
Unfortunately, I have not found a better solution than to intercept the Access Token event. Then I parsed it and manually added claims to the cookie.
options.Events = new OpenIdConnectEvents
{
OnTokenResponseReceived = xxx =>
{
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
JwtSecurityToken jwt = handler.ReadJwtToken(xxx.TokenEndpointResponse.AccessToken);
var claimsIdentity = (ClaimsIdentity) xxx.Principal.Identity;
claimsIdentity.AddClaims(jwt.Claims);
return Task.FromResult(0);
}
};
I will be very grateful to you! If you look at the source code of the project (it has been updated to asp.net core 2.1) and offer the best option!

ASP.NET 5 Identity 3.0 scalability with CookieAuthentication

I'm using ASP.NET 5 with MVC6. I am working with Identity 3.0, but I need to know how to make it works with many webservers.
Is possible to store the session in other place? Database? In MVC5 you did that in the web.config, but I don't found information about it in MVC6.
This is my code in Startup.cs
app.UseCookieAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.LoginPath = new PathString("/Account/Login");
options.AutomaticChallenge = true;
});
Thanks!!
By default, authentication tickets stored in cookies are self-contained: knowing the encryption key is enough to retrieve the original ticket (there's no store or database involved in this process).
To make sure your authentication cookies are readable by all your servers, you need to synchronize the key ring they use to encrypt and decrypt authentication tickets. This can be done using an UNC share, as mentioned by the documentation: http://docs.asp.net/en/latest/security/data-protection/configuration/overview.html.
public void ConfigureServices(IServiceCollection services) {
services.AddDataProtection();
services.ConfigureDataProtection(options => {
options.PersistKeysToFileSystem(new DirectoryInfo(#"\\server\share\directory\"));
});
}
Alternatively, you could also provide your own TicketDataFormat to override the serialization/encryption logic, but it's definitely not the recommended approach.

Categories

Resources