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?
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
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
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.
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.
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;
}
}
}
}
Related
So I'm using the ASP.NET Core React Template with built-in authorization. In that template, everything is working and I'm able to login and register an account via this
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
When I view the token via application localstorage, I get the following data. Without the role.
I also viewed the access token via jwt.io
My question is, how can I add the role there or the role in the jwt token?
Thank you!
You need to create a ProfileService which implements IProfileService interface
I share you code from my project
public class ProfileService : IProfileService
{
protected UserManager<ApplicationUser> UserManager;
public ProfileService(UserManager<ApplicationUser> userManager)
{
UserManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
ApplicationUser user = await UserManager.GetUserAsync(context.Subject);
IList<string> roles = await UserManager.GetRolesAsync(user);
var claims = new List<Claim> {
// here you can include other properties such as id, email, address, etc. as part of the jwt claim types
new Claim(JwtClaimTypes.Email, user.Email),
new Claim(JwtClaimTypes.Name, $"{user.Firstname} {user.Lastname}")
};
foreach (string role in roles)
{
// include the roles
claims.Add(new Claim(JwtClaimTypes.Role, role));
}
context.IssuedClaims.AddRange(claims);
}
public Task IsActiveAsync(IsActiveContext context)
{
return Task.CompletedTask;
}
}
Add DI registration to Startup
services.AddTransient<IProfileService, ProfileService>();
Details in IdentityServer4 documentation
We're using System.Web.Security.FormsAuthenticationTicket to create anonymous cookies for users that aren't logged in. Is there an equivalent in AspNetCore?
I'm well aware that ASP.NET Core cannot support forms authentication. The new way of doing things is cookies. So how to create a cookie that does the equivalent in the new situation?
Asp.net core cannot support form authentication. I recommend you use cookie-base authentication. This link can help you build it.
If you want to skip a method that requires authorized access. You can add attribute [AllowAnonymous].
[AllowAnonymous]
public IActionResult Privacy()
{
return View();
}
Or you can refer to this link.
Configure cookie in Startup.cs.
services.AddAuthentication("auth")
.AddCookie("auth",config=>
{
config.Cookie.Name = "cookie.name";
config.LoginPath = "/home/login";
});
Generate token in this action. You can fill the claim by receiving form data.
[HttpPost]
public IActionResult login()
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name,"myName"),
new Claim(ClaimTypes.Role,"myRole")
};
var claimIdentity = new ClaimsIdentity(claims,"id card");
var claimPrinciple = new ClaimsPrincipal(claimIdentity);
var authenticationProperty = new AuthenticationProperties
{
IsPersistent = true
};
HttpContext.SignInAsync(claimPrinciple,authenticationProperty);
return View();
}
We have a .Net Core 2.0 Web API project. I have added the hangfire there. We don't have any web page in the project and I use JWT for authorization. So I'm not able to do the authorization for hangfire using the Authorize(DashboardContext context). Is there any way we can pass some sort of API key on the url to authorize the user for dashboard?
Thanks
Yes, you can do that by using cookies, I will explain the idea to you with some code...
Firstly once the user login (you generate the token) you must store the token you generated to cookies in the browser and then when you want to access to Hangfire dashboard you must read the token from cookies and then check the roles...
the code to store the token in cookies:
`
httpContext.Response.Cookies.Append("token", userToken.AccessToken,
new Microsoft.AspNetCore.Http.CookieOptions { Expires = DateTime.Now.AddMinutes(6000) });
`
make sure you enabled cookies by:
`
AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie()
.AddJwtBearer(cfg => your configs);
`
then the authorize method will be like this:
`
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
var jwtToken = string.Empty;
if (httpContext.Request.Cookies.ContainsKey("token"))
{
httpContext.Request.Cookies.TryGetValue("token", out jwtToken);
}
else
return false;
if (string.IsNullOrEmpty(jwtToken))
{
return false;
}
var handler = new JwtSecurityTokenHandler();
try
{
var claim = _tokenService.GetClaimsPrincipal(jwtToken);
return claim != null && claim.IsInRole(RolesConstants.ADMIN);
}
catch (Exception exception)
{
throw exception;
}
}
`
Implement an IAuthorizationDashboardFilter class.
Try this post here.
You need to pass JWT token with each request, try storing token in a cookie or as a query string parameter. Then you can pull that from request context and decide whether user is authorized
public class MyAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
var token = context.Request.Cookies["access-token"];
// check token validity and set authentication accordingly.
return httpContext.User.Identity.IsAuthenticated;
}
}
Register the filter in your OWIN pipeline like this, after whatever authentication method you are using. Then the logged in user claim will be available in the filter
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(...); // Authentication - first
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new [] { new MyAuthorizationFilter() }
}); // Hangfire - last
}
Following on from this question, I ended up using the HttpContext.SignInAsync(string subject, Claim[] claims) overload to pass the selected tenant id as a claim (defined as type "TenantId") after the user selects a Tenant.
I then check for this claim in my custom AccountChooserResponseGenerator class from this question to determine if the user needs to be directed to the Tenant Chooser page or not, as follows:
public override async Task<InteractionResponse> ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
{
var response = await base.ProcessInteractionAsync(request, consent);
if (response.IsConsent || response.IsLogin || response.IsError)
return response;
if (!request.Subject.HasClaim(c=> c.Type == "TenantId" && c.Value != "0"))
return new InteractionResponse
{
RedirectUrl = "/Tenant"
};
return new InteractionResponse();
}
The interaction is working and the user gets correctly redirected back to the Client app after selecting a Tenant.
However, on my client, I have the simple:
<dl>
#foreach (var claim in User.Claims)
{
<dt>#claim.Type</dt>
<dd>#claim.Value</dd>
}
</dl>
snippet from the IdentityServer4 quickstarts to show the claims, and sadly, my TenantId claim is not there.
I have allowed for it in the definition of my Client on my IdentityServer setup, as follows:
var client = new Client
{
... other settings here
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Phone,
"TenantId"
}
};
What am I missing in order for this TenantId claim to become visible in my Client application?
EDIT:
Based on #d_f's comments, I have now added TentantId to my server's GetIdentityResources(), as follows:
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResources.Phone(),
new IdentityResource("TenantId", new[] {"TenantId"})
};
}
And I have edited the client's startup.ConfigureServices(IServiceCollection services) to request this additional scope, as follows:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
//other settings not shown
options.Scope.Add("TenantId");
});
And still the only claims displayed on the client by the indicated snippet are:
Edit 2: Fixed!
Finally #RichardGowan's answer worked. And that is because (as brilliantly observed by #AdemCaglin) I was using IdentityServer's AspNetIdentity, which has it's own implementation of IProfileService, which kept dropping my custom TenantId claim, despite ALL these other settings).
So in the end, I could undo all those other settings...I have no mention of the TenantId claim in GetIdentityResources, no mention of it in AllowedScopes in the definition of the Client in my IdSrv, and no mention of it in the configuration of services.AddAuthentication on my client.
You will need to provide and register an implementation of IProfileService to issue your custom claim back to the client:
public class MyProfileService : IProfileService {
public MyProfileService() {
}
public Task GetProfileDataAsync(ProfileDataRequestContext context) {
// Issue custom claim
context.IssuedClaims.Add(context.Subject.Claims.First(c => c.Type ==
"TenantId"));
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context) {
context.IsActive = true;
return Task.CompletedTask;
}
}
Trying to configure my Web Api as Resource Server. My client logs into Auth0 and gets Bearer token, so Authorization Server is Auth0 not my Api. Then they send request along with the Bearer token to my Api. In my ASP.Net Web Api I have implemented following OWIN configuration in Startup class to validate the request JWT Bearer token issued by Auth0 as instructed here.
Statup:
public class Startup
{
public void Configuration(IAppBuilder app)
{
var auth0Options = new Auth0Options()
{
Issuer = $"https://{ConfigurationManager.AppSettings["Auth0ApiInternalDomain"]}/",
Audience = ConfigurationManager.AppSettings["Auth0ApiInternalAudience"],
ClientId = ConfigurationManager.AppSettings["Auth0ApiInternalClientID"]
};
Auth0Config.Configure(app, auth0Options);
// Configure Web API
WebApiConfig.Configure(app);
}
}
and Auth0Config class:
public class Auth0Config
{
public static void Configure(IAppBuilder app, Auth0Options options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
var keyResolver = new OpenIdConnectSigningKeyResolver(options.Issuer);
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = options.Audience,
ValidIssuer = options.Issuer,
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) => keyResolver.GetSigningKey(identifier),
ValidateLifetime = true,
ValidateIssuer = true,
ValidateAudience = true,
LifetimeValidator = (DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) =>
{
if (expires.Value < DateTime.UtcNow)
{
return false;
}
return true;
}
}
});
}
}
I pass Audience, Issuer and CliedntId from my app.config to this method. My intention is to figure out whether the Bearer token coming from the client to my Api is valid or not (here as first step I need to validate expiration date). When I debug my code for the incoming request, LifetimeValidator works fine and returns false for the expired token. I decorated my action with [Authorize] and expected to get 401 error but the actual response is 200 and it seems it ignores the LifetimeValidator implementation.
My action:
[Authorize]
public IHttpActionResult Get(string id)
{
var result = _bookingService.GetBooking(id);
if (result == null) return NotFound();
return Ok(result);
}
Am I missing something to get it right?
Is this a good approach to validate token expiration?
Is it possible to use OWIN only to validate the request Bearer token that has been issued out of web api application?
It turned out Invoke method of OwinMiddleware class had been overridden in my application to find Username from token and inject it to Request.User. Not sure why but somehow it ignores OWIN token validation functionality and didn't check Audience, Issuer or Expiration time.
public static void Configure(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
app.Use<HttpUsernameInjector>();
app.UseWebApi(config);
}
public class HttpUsernameInjector : OwinMiddleware
{
public HttpUsernameInjector(OwinMiddleware next)
: base(next){}
public override async Task Invoke(IOwinContext context)
{
const string usernameClaimKey = "myUserNameClaimKey";
var bearerString = context.Request.Headers["Authorization"];
if (bearerString != null && bearerString.StartsWith("Bearer ", StringComparison.InvariantCultureIgnoreCase))
{
var tokenString = bearerString.Substring(7);
var token = new JwtSecurityToken(tokenString);
var claims = token.Claims.ToList();
var username = claims.FirstOrDefault(x => x.Type == usernameClaimKey);
if (username == null) throw new Exception("Token should have username");
// Add to HttpContext
var genericPrincipal = new GenericPrincipal(new GenericIdentity(username.Value), new string[] { });
IPrincipal principal = genericPrincipal;
context.Request.User = principal;
}
await Next.Invoke(context);
}
}
by removing this class, OWIN token validation works fine!
Base on my research, the best token validation approaches in Web Api are OWIN and also IAuthenticationFilter.
It is possible as Resource Server and Authorization Server are decoupled. More info can be found here
Update
Found the solution here to stop OwinMiddleware suppressing my token validation logic