I'm trying to skip the token validation for a methods I want to make "public" on my API.
In my StartUp I got the below event to check if a call is authorized:
x.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var sessionManager = context.HttpContext.GetService<ISessionManager>();
if (!sessionManager.IsCurrentTokenValid())
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
var message = Encoding.UTF8.GetBytes("invalidToken");
context.Response.OnStarting(async () =>
{
await context.Response.Body.WriteAsync(message, 0, message.Length);
});
}
}
};
I've tried to remove [Authorize] attribute form the controller but the above code still triggers
Also tried to add [IgnoreAntiforgeryToken(Order = 1001)] on the method I want to skip the validation but still the above code triggers.
Do you know how can I disable it only for certain methods ?
Usually you decorate controllers or actions which you want to allow w/o authentication with [AllowAnonymous] (see docs).
If you have multiple authentications (Jwt, Cookie) and you want specific endpoints only allowed with a specific authentication, you use the scheme attribute, i.e. [Authorize(Scheme = "Cookie)].
Try to ignore token validation result if the the endpoint implements AllowAnonymous
x.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var sessionManager = context.HttpContext.GetService<ISessionManager>();
var endpoint = context.HttpContext.Features.Get<IEndpointFeature>()?.Endpoint;
var allowAnon = endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null;
if (!allowAnon && !sessionManager.IsCurrentTokenValid())
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
var message = Encoding.UTF8.GetBytes("invalidToken");
context.Response.OnStarting(async () =>
{
await context.Response.Body.WriteAsync(message, 0, message.Length);
});
}
}
};
Related
When using the ASP.Net core 3.0 angular SPA individual account template, and the AspNet.Security.OAuth.Spotify nuget package. When A user logs in, I want to be able to get their spotify access token so I can preform actions on the user's behalf. However, when I call await HttpContext.GetTokenAsync("spotify", "access_token"); the results returns null.
I've debugged a bit and saw the spotify tokens on second+ login in the options.Events.OnCreatingTicket event, but I guess the token is just not passed around past that? I'm not really sure anymore.
Startup.cs
ConfigureServices
services.AddAuthentication()
.AddIdentityServerJwt()
.AddSpotify("spotify", options =>
{
options.ClientId = Configuration["SpotifySettings:ClientId"];
options.ClientSecret = Configuration["SpotifySettings:ClientSecret"];
options.CallbackPath = "/callback";
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SaveTokens = true;
String[] items = {
"playlist-read-private", "playlist-modify-public", "playlist-modify-private", "playlist-read-collaborative", "user-library-modify", "user-library-read", "user-read-email"
};
foreach (var item in items)
{
options.Scope.Add(item);
}
options.Events.OnRemoteFailure = (context) =>
{
// Handle failed login attempts here
return Task.CompletedTask;
};
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
});
Code to try to retrieve token
if (!User.Identity.IsAuthenticated)
{
return StatusCode(403);
}
var client = httpClientFactory.CreateClient("spotify");
String spotifyToken = await HttpContext.GetTokenAsync("spotify", "access_token");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", spotifyToken);
var result = await client.GetAsync("v1/me/playlists");
I expected to get a spotify access_token that I can use to call the spotify api but spotifyToken just returns null.
On your callback handling method, in case of IdentityServer, on the ExternalController you read out the external identity of the temporary cookie - the result (in case of a success) holds both the Access & Refresh Token as Properties:
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result.Succeeded != true)
throw new Exception("External authentication error");
var tokens = result.Properties.GetTokens();
if (tokens?.Any() == true)
{
_logger.LogDebug("External authentication success resulted in provided access tokens: \r\n {0}.", string.Join(",", tokens.Select(d => $"{d.Name}:{d.Value}")));
}
I'm using ASP.Net Core 2.1 with IdentityCore Service, the application is a pure API, no views at all. For authentication I'm purely using Steam authentication (No User/Pass login) provided by, https://github.com/aspnet-contrib/AspNet.Security.OpenId.Providers
This API has been created to fit a very specific authentication workflow (User's can only login to the API with Steam) the Angular SPA sitting as the frontend handles the workflow just fine.
The problem is that when I add a Role to a user (I've already got roles seeded and I've added my own steam account to the Admin Role), the role type claims are not being added on login, therefore when an admin user attempts to access an API route protected by [Authorize(Roles = "Admin") I'm being returned an Unauthorized Redirect.
Below I have added all code snippets I think is required (feel free to request more).
If I use (I am currently using this as a temporary solution, but it is not ideal for future development);
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<RSContext>()
.AddSignInManager<SignInManager<User>>()
.AddRoleManager<RoleManager<Role>>()
.AddDefaultTokenProviders();
The application correctly adds the role claims on user sign in (and the Authorize attributes work), using all existing code from AuthController.cs, yet using IdentityCore it fails. I feel I am missing a single line that is responsible for this but after trolling MSDN docs for days I am finally outwitted.
NOTE: The API will correctly authenticate and set the users cookies on sign in, but does not add the users roles to the users identity claims. Therefore, Authentication is Working, Authorization is not. If I utilise the [Authorize] attribute without specifying a Role it works flawlessly and only allows Authenticated users to access the route whilst denying unAuthenticated users. This can be seen in the Testing Screenshot at the end, identities[0].isAuthenticated = True, but the admin role is not being added to the Identity's Claims. As noted above, if I do not use AddIdentityCore and use AddIdentity, the roles are added to the user's claims correctly and the [Authorize(Role = "Admin")] attribute will work as expected, only allowing users that are apart of the Admin role to access it.
Startup.cs (Omitted irrelevant parts, eg. Database Connection)
public void ConfigureServices(IServiceCollection services)
{
IdentityBuilder builder = services.AddIdentityCore<User>(opt =>
{
opt.Password.RequireDigit = true;
opt.Password.RequiredLength = 6;
opt.Password.RequireNonAlphanumeric = true;
opt.Password.RequireUppercase = true;
opt.User.AllowedUserNameCharacters += ":/";
});
builder = new IdentityBuilder(builder.UserType, typeof(Role), builder.Services);
builder.AddEntityFrameworkStores<RSContext>();
builder.AddSignInManager<SignInManager<User>>();
builder.AddRoleValidator<RoleValidator<Role>>();
builder.AddRoles<Role>();
builder.AddRoleManager<RoleManager<Role>>();
builder.AddClaimsPrincipalFactory<UserClaimsPrincipalFactory<User>>();
builder.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignOutScheme = IdentityConstants.ApplicationScheme;
options.DefaultForbidScheme = IdentityConstants.ApplicationScheme;
})
.AddSteam(options =>
{
options.ApplicationKey = Configuration.GetSection("Authentication:Steam:Key").Value;
options.CallbackPath = "/api/auth/steam/callback";
options.Events.OnAuthenticated = OnClientAuthenticated;
})
.AddIdentityCookies(options =>
{
options.ApplicationCookie.Configure(appCookie =>
{
appCookie.Cookie.Name = "RaidSimulator";
appCookie.LoginPath = "/api/auth/login";
appCookie.LogoutPath = "/api/auth/logout";
appCookie.Cookie.HttpOnly = true;
appCookie.Cookie.SameSite = SameSiteMode.Lax;
appCookie.Cookie.IsEssential = true;
appCookie.SlidingExpiration = true;
appCookie.Cookie.Expiration = TimeSpan.FromMinutes(1);
appCookie.Cookie.MaxAge = TimeSpan.FromDays(7);
});
options.ExternalCookie.Configure(extCookie =>
{
extCookie.Cookie.Name = "ExternalLogin";
extCookie.LoginPath = "/api/auth/login";
extCookie.LogoutPath = "/api/auth/logout";
extCookie.Cookie.HttpOnly = true;
extCookie.Cookie.SameSite = SameSiteMode.Lax;
extCookie.Cookie.IsEssential = true;
extCookie.Cookie.Expiration = TimeSpan.FromMinutes(10);
});
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, RoleManager<Role> roleManager)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
RolesSeed.Seed(roleManager).Wait();
app.UseCors();
app.UseAuthentication();
app.UseMvc();
}
// Responsible for storing/updating steam profile in database
private async Task OnClientAuthenticated(OpenIdAuthenticatedContext context)
{
var rsContext = context.HttpContext.RequestServices.GetRequiredService<RSContext>();
var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<User>>();
var profile = context.User?.Value<JObject>(SteamAuthenticationConstants.Parameters.Response)
?.Value<JArray>(SteamAuthenticationConstants.Parameters.Players)?[0]?.ToObject<SteamProfile>();
// TODO: Handle this better, Redir user to an informative error page or something
if (profile == null)
return;
var dbProfile = await rsContext.SteamProfiles.FindAsync(profile.SteamId);
if (dbProfile != null)
{
rsContext.Update(dbProfile);
dbProfile.UpdateProfile(profile);
await rsContext.SaveChangesAsync();
}
else
{
await rsContext.SteamProfiles.AddAsync(profile);
await rsContext.SaveChangesAsync();
}
}
AuthController.cs => The only code responsible for authenticating against the Identity.Application scheme
[HttpGet("callback")]
[Authorize(AuthenticationSchemes = "Steam")]
public async Task<IActionResult> Callback([FromQuery]string ReturnUrl)
{
ReturnUrl = ReturnUrl?.Contains("api/") == true ? "/" : ReturnUrl;
if (HttpContext.User.Claims.Count() > 0)
{
var provider = HttpContext.User.Identity.AuthenticationType;
var nameIdentifier = HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
var name = HttpContext.User.FindFirstValue(ClaimTypes.Name);
var loginResult = await signInManager.ExternalLoginSignInAsync(provider, nameIdentifier, false);
if (loginResult.Succeeded)
{
return Redirect(ReturnUrl ?? "/api/auth/claims");
}
var result = await userManager.CreateAsync(new User { UserName = nameIdentifier, SteamId = nameIdentifier.Split("/").Last() });
if (result.Succeeded)
{
var user = await userManager.FindByNameAsync(nameIdentifier);
var identity = await userManager.AddLoginAsync(user, new UserLoginInfo(provider, nameIdentifier, name));
if (identity.Succeeded)
{
await signInManager.ExternalLoginSignInAsync(provider, nameIdentifier, false);
return Redirect(ReturnUrl ?? "/api/auth/claims");
}
}
}
return BadRequest(new { success = false });
}
[HttpGet("claims")]
[Authorize]
public async Task<IActionResult> GetClaims()
{
var user = await userManager.GetUserAsync(User);
var claims =
User.Claims.Select(c => new
{
c.Type,
c.Value
});
var inAdmin = new string[] {
"User.IsInRole(\"Admin\") = " + User.IsInRole("Admin"),
"User.IsInRole(\"ADMIN\") = " + User.IsInRole("ADMIN"),
"User.IsInRole(\"admin\") = " + User.IsInRole("admin"),
"userManager.IsInRoleAsync(user, \"admin\") = " + await userManager.IsInRoleAsync(user, "admin")
};
return Ok(new { success = true, data = new { claims, inAdmin, User.Identities } });
}
RoleSeeder.cs
public static async Task Seed(RoleManager<Role> roleManager)
{
// Developer Role
if(!await roleManager.RoleExistsAsync("Developer"))
{
var role = new Role("Developer");
await roleManager.CreateAsync(role);
}
// Community Manager Role
if (!await roleManager.RoleExistsAsync("Community Manager"))
{
var role = new Role("Community Manager");
await roleManager.CreateAsync(role);
}
// Admin Role
if (!await roleManager.RoleExistsAsync("Admin"))
{
var role = new Role("Admin");
await roleManager.CreateAsync(role);
}
// Moderator Role
if (!await roleManager.RoleExistsAsync("Moderator"))
{
var role = new Role("Moderator");
await roleManager.CreateAsync(role);
}
}
Testing Screenshot:
claims/identities/roletest API Response
Posted this issue to ASP.Net Identity GitHub repo, it is a known bug and has been resolved in ASP.Net Core 2.2
Link: https://github.com/aspnet/Identity/issues/1997
You have tow way for this issue.
When you send a request to any WebService, If you set, Authorization be run Now:
Before login If you want to send a request to your WebService, and want to ignore Authorization you have to use of Allowanonymus Attribute like:
[Allowanonymous]
Public void Login()
{
// here
}
With this attribute, Authorization will ignore the request.
Now! If you want to send a request after login, you should to create you cookie In login time, and send a response to client, and also you have save that cookie in your localStorage in client, for Identifying in client. After this you have to set that cookie in
header of every request. With this way, you authorization will Don!
Now If you want, I can create a sample for Authorization in best practice.
When a user logs out under certain circumstances I want to show them a message on the logged out page. To enable this I want to be able to send an optional parameter from the client to the Identity Server / Authority site on logout.
While I have the standard logout flow working I have hit a brick wall in handling this scenario as information seems thin on the ground and the suggested solutions are not working.
From what I have read the 'state' parameter is the correct way to pass this information but this not coming through currently. AcrValues are only used to send information the other way.
My naive implementation below simply adds a state query string item to the end session endpoint. However, when I check the query string my client uses to go to the identity server instance it is missing.
Redirect(discoveryResponse.EndSessionEndpoint+"&state=foo")
Any help gladly received!
Current flow for MVC client:
Please note; some code has been removed for brevity.
Logout initiated from client controller with state=foo:
public class LogoutController : Controller
{
public ActionResult Index()
{
Request.GetOwinContext().Authentication.SignOut();
var discoveryClient = new DiscoveryClient(clientConfig.Authority) { Policy = {RequireHttps = false} };
var discoveryResponse = discoveryClient.GetAsync().Result;
var tokenClaim = ((ClaimsIdentity)User.Identity).FindFirst("id_token");
return Redirect(discoveryResponse.EndSessionEndpoint+ "?id_token_hint="+ tokenClaim + "&state=foo");
}
}
RedirectToIdentityProvider is called for request:
IdTokenHint and PostLogoutRedirectUri are set and passed correctly.
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType != OpenIdConnectRequestType.LogoutRequest)
return Task.FromResult(0);
var idTokenHint = n.OwinContext.Authentication.User.FindFirst(OpenIdConnectClaimType.IdToken);
if (idTokenHint == null) return Task.FromResult(0);
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
n.OwinContext.Response.Cookies.Append("IdentityServerPostLogoutReturnUri",
n.ProtocolMessage.PostLogoutRedirectUri);
n.ProtocolMessage.PostLogoutRedirectUri =
n.Options.PostLogoutRedirectUri;
return Task.FromResult(0);
}
}
URL Generated (not the lack of 'state' item):
http://localhost:44362/connect/endsession?post_logout_redirect_uri=http%3a%2f%2flocalhost%3a2577%2fpostloginredirect&id_token_hint=removed&x-client-SKU=ID_NET&x-client-ver=1.0.40306.1554
Logout page on the authority site:
This is where I want to be able to access the state parameter.
public class LogoutController : Controller
{
public async Task<ViewResult> Index(string logoutId)
{
if (logoutId == null) throw new Exception("Missing logoutId");
var logoutRequest = await interactionService.GetLogoutContextAsync(logoutId);
var vm = new LoggedOutViewModel(logoutRequest, logoutId);
if (!string.IsNullOrWhiteSpace(httpContextService.GetCookieValue(PostLogoutReturnUriCookieKey)))
{
vm.PostLogoutRedirectUri = httpContextService.GetCookieValue(PostLogoutReturnUriCookieKey);
httpContextService.ClearCookie(PostLogoutReturnUriCookieKey);
}
await httpContextService.SignOutAsync();
return View("Index", vm);
}
}
I've dug a little deeper and found what the issue was being caused by the following lines in the Microsoft.Owin.Security.OpenIdConnect middleware.
protected override async Task ApplyResponseGrantAsync()
{
AuthenticationResponseRevoke signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode);
if (signout != null)
{
// snip
var notification = new RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = openIdConnectMessage
};
await Options.Notifications.RedirectToIdentityProvider(notification);
// This was causing the issue
if (!notification.HandledResponse)
{
string redirectUri = notification.ProtocolMessage.CreateLogoutRequestUrl();
if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
{
_logger.WriteWarning("The logout redirect URI is malformed: " + redirectUri);
}
Response.Redirect(redirectUri);
}
}
}
In order to prevent the middleware from overriding the redirect when it detects a sign out message the following line in the 'HandleResponse' method needs to be called in the RedirectToIdentityProvider event.
This allows the original 'state' query string item to be passed to Identity Server and be pulled out using the interaction service.
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
// Snip
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// Snip
},
RedirectToIdentityProvider = n =>
{
// Snip
n.HandleResponse(); // The magic happens here
}
}
I've got a working solution for this, but I'm wondering if this is the correct way to do it. Here's what I got so far.
I'm using ASP.Net Core 1.1.2 with ASP.NET Core Identity 1.1.2.
The important part in Startup.cs looks like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//...
app.UseFacebookAuthentication(new FacebookOptions
{
AuthenticationScheme = "Facebook",
AppId = Configuration["ExternalLoginProviders:Facebook:AppId"],
AppSecret = Configuration["ExternalLoginProviders:Facebook:AppSecret"]
});
}
FacebookOptionscomes with Microsoft.AspNetCore.Authentication.Facebook nuget package.
The callback function in AccountController.cs looks like this:
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
//... SignInManager<User> _signInManager; declared before
ExternalLoginInfo info = await _signInManager.GetExternalLoginInfoAsync();
SignInResult signInResult = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
byte[] thumbnailBytes = null;
if (info.LoginProvider == "Facebook")
{
string nameIdentifier = info.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
string thumbnailUrl = $"https://graph.facebook.com/{nameIdentifier}/picture?type=large";
using (HttpClient httpClient = new HttpClient())
{
thumbnailBytes = await httpClient.GetByteArrayAsync(thumbnailUrl);
}
}
//...
}
So this code is working absolutely fine but, as mentioned before, is this the correct way (technically, not opinion-based) to do it?
To get profile picture from Facebook, you need to configure Facebook options and subscribe at OnCreatingTicket event from OAuth.
services.AddAuthentication().AddFacebook("Facebook", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = configuration.GetSection("ExternalLogin").GetSection("Facebook").GetSection("ClientId").Value;
options.ClientSecret = configuration.GetSection("ExternalLogin").GetSection("Facebook").GetSection("ClientSecret").Value;
options.Fields.Add("picture");
options.Events = new OAuthEvents
{
OnCreatingTicket = context =>
{
var identity = (ClaimsIdentity)context.Principal.Identity;
var profileImg = context.User["picture"]["data"].Value<string>("url");
identity.AddClaim(new Claim(JwtClaimTypes.Picture, profileImg));
return Task.CompletedTask;
}
};
});
In ASP.NET Core 3.0 there was a breaking change in OAuthCreatingTicketContext, see https://learn.microsoft.com/en-US/dotnet/core/compatibility/2.2-3.0
I changed
var profileImg = context.User["picture"]["data"].Value<string>("url");
to
var profileImg = context.User.GetProperty("picture").GetProperty("data").GetProperty("url").ToString();
I got the image from the graph api only using the identifier
$"https://graph.facebook.com/{identifier}/picture?type=large";
There is also a possibility to use custom claim actions to map json user content to claims (claim type to use is up to you). So image url will be added to claims collection, no need in OAuthEvents (if you don't need them for other purposes).
.AddFacebook("FbCustom", x =>
{
x.AppId = settings.FacebookAppId;
x.AppSecret = settings.FacebookSecret;
x.Scope.Add("email");
x.Scope.Add("user_hometown");
x.Scope.Add("user_birthday");
x.Fields.Add("birthday");
x.Fields.Add("picture");
x.Fields.Add("name");
x.Fields.Add("email");
//HERE IS CUSTOM A MAPPING
x.ClaimActions.MapCustomJson(CustomClaimTypes.AvatarUrl,
json => json.GetProperty("picture").GetProperty("data").GetProperty("url").GetString());
})
in asp.net core 3.1, I did this by calling Facebook APIs directly with the access token returned when the authentication is done.
Here is the process:
In a controller method, you can challenge.
var auth = await Request.HttpContext.AuthenticateAsync("Facebook");
This will redirect the user to Facebook login in the browser.
If the authentication succeeds, that is: auth.Succeeded && auth.Principal.Identities.Any(id => id.IsAuthenticated) && ! string.IsNullOrEmpty(auth.Properties.GetTokenValue("access_token")
Retrieve the authentication token facebook provided like this: auth.Properties.GetTokenValue("access_token")
Then use the token to get the user's profil picture manually like this:
public async Task<string> GetFacebookProfilePicURL(string accessToken)
{
using var httpClient = new HttpClient();
var picUrl = $"https://graph.facebook.com/v5.0/me/picture?redirect=false&type=large&access_token={accessToken}";
var res = await httpClient.GetStringAsync(picUrl);
var pic = JsonConvert.DeserializeAnonymousType(res, new { data = new PictureData() });
return pic.data.Url;
}
Where PictureData is just a class representing the response from Facebook's graph API with all the info about the picture; Height, Width, url etc.
I've got a Web API project fronted by Angular, and I want to secure it using a JWT token. I've already got user/pass validation happening, so I think i just need to implement the JWT part.
I believe I've settled on JwtAuthForWebAPI so an example using that would be great.
I assume any method not decorated with [Authorize] will behave as it always does, and that any method decorated with [Authorize] will 401 if the token passed by the client doesn't match.
What I can't yet figure out it how to send the token back to the client upon initial authentication.
I'm trying to just use a magic string to begin, so I have this code:
RegisterRoutes(GlobalConfiguration.Configuration.Routes);
var builder = new SecurityTokenBuilder();
var jwtHandler = new JwtAuthenticationMessageHandler
{
AllowedAudience = "http://xxxx.com",
Issuer = "corp",
SigningToken = builder.CreateFromKey(Convert.ToBase64String(new byte[]{4,2,2,6}))
};
GlobalConfiguration.Configuration.MessageHandlers.Add(jwtHandler);
But I'm not sure how that gets back to the client initially. I think I understand how to handle this on the client, but bonus points if you can also show the Angular side of this interaction.
I ended-up having to take a information from several different places to create a solution that works for me (in reality, the beginnings of a production viable solution - but it works!)
I got rid of JwtAuthForWebAPI (though I did borrow one piece from it to allow requests with no Authorization header to flow through to WebAPI Controller methods not guarded by [Authorize]).
Instead I'm using Microsoft's JWT Library (JSON Web Token Handler for the Microsoft .NET Framework - from NuGet).
In my authentication method, after doing the actual authentication, I create the string version of the token and pass it back along with the authenticated name (the same username passed into me, in this case) and a role which, in reality, would likely be derived during authentication.
Here's the method:
[HttpPost]
public LoginResult PostSignIn([FromBody] Credentials credentials)
{
var auth = new LoginResult() { Authenticated = false };
if (TryLogon(credentials.UserName, credentials.Password))
{
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, credentials.UserName),
new Claim(ClaimTypes.Role, "Admin")
}),
AppliesToAddress = ConfigurationManager.AppSettings["JwtAllowedAudience"],
TokenIssuerName = ConfigurationManager.AppSettings["JwtValidIssuer"],
SigningCredentials = new SigningCredentials(new
InMemorySymmetricSecurityKey(JwtTokenValidationHandler.SymmetricKey),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
"http://www.w3.org/2001/04/xmlenc#sha256")
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
auth.Token = tokenString;
auth.Authenticated = true;
}
return auth;
}
UPDATE
There was a question about handling the token on subsequent requests. What I did was create a DelegatingHandler to try and read/decode the token, then create a Principal and set it into Thread.CurrentPrincipal and HttpContext.Current.User (you need to set it into both). Finally, I decorate the controller methods with the appropriate access restrictions.
Here's the meat of the DelegatingHandler:
private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
{
token = null;
IEnumerable<string> authzHeaders;
if (!request.Headers.TryGetValues("Authorization", out authzHeaders) || authzHeaders.Count() > 1)
{
return false;
}
var bearerToken = authzHeaders.ElementAt(0);
token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
return true;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpStatusCode statusCode;
string token;
var authHeader = request.Headers.Authorization;
if (authHeader == null)
{
// missing authorization header
return base.SendAsync(request, cancellationToken);
}
if (!TryRetrieveToken(request, out token))
{
statusCode = HttpStatusCode.Unauthorized;
return Task<HttpResponseMessage>.Factory.StartNew(() => new HttpResponseMessage(statusCode));
}
try
{
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
TokenValidationParameters validationParameters =
new TokenValidationParameters()
{
AllowedAudience = ConfigurationManager.AppSettings["JwtAllowedAudience"],
ValidIssuer = ConfigurationManager.AppSettings["JwtValidIssuer"],
SigningToken = new BinarySecretSecurityToken(SymmetricKey)
};
IPrincipal principal = tokenHandler.ValidateToken(token, validationParameters);
Thread.CurrentPrincipal = principal;
HttpContext.Current.User = principal;
return base.SendAsync(request, cancellationToken);
}
catch (SecurityTokenValidationException e)
{
statusCode = HttpStatusCode.Unauthorized;
}
catch (Exception)
{
statusCode = HttpStatusCode.InternalServerError;
}
return Task<HttpResponseMessage>.Factory.StartNew(() => new HttpResponseMessage(statusCode));
}
Don't forget to add it into the MessageHandlers pipeline:
public static void Start()
{
GlobalConfiguration.Configuration.MessageHandlers.Add(new JwtTokenValidationHandler());
}
Finally, decorate your controller methods:
[Authorize(Roles = "OneRoleHere")]
[GET("/api/admin/settings/product/allorgs")]
[HttpGet]
public List<Org> GetAllOrganizations()
{
return QueryableDependencies.GetMergedOrganizations().ToList();
}
[Authorize(Roles = "ADifferentRoleHere")]
[GET("/api/admin/settings/product/allorgswithapproval")]
[HttpGet]
public List<ApprovableOrg> GetAllOrganizationsWithApproval()
{
return QueryableDependencies.GetMergedOrganizationsWithApproval().ToList();
}