I am developing a web service using ASP.NET Web API. I am using ASP.NET Identity for authentication and token generation. I need to return an extended property in token response json. Till now I am able to return an extended string property in which I am sending a json string obtained by serializing a custom class object into json. Following is my auth provider code:
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
await Task.Run(() =>
{
context.Validated();
});
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
await Task.Run(() =>
{
var loginResponse = new AccountManager().Login(context.UserName, context.Password);
if (loginResponse == null)
{
context.SetError("invalid_grant", Resources.Messages.InvalidGrant);
return;
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userData", JsonConvert.SerializeObject(loginResponse) }
};
AuthenticationProperties properties = new AuthenticationProperties(data);
Microsoft.Owin.Security.AuthenticationTicket ticket = new Microsoft.Owin.Security.AuthenticationTicket(identity, properties);
context.Validated(ticket);
});
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
}
Now in my response I have a property e.g. "userData" : "<Json String>" whereas I wanted to assign a json object (not json string) to userData. Is it possible?
I do not recommend putting JSON object inside ticket properties, this will INCREASE token size big time, and you be transmitting this token with each request.
Maybe it is better if you define protected standalone endpoint to do this task after you obtain the access token. You will issue extra Get request after successful login but you will keep token size minimal.
Related
I am attempting to build a super simple API-key authentication for certain APIs in a controller. For this I have this in ConfigureServices()
services.AddAuthorization(options =>
{
options.AddPolicy(Auth.Constants.WebmasterPolicyName, policy =>
policy.RequireAssertion(context =>
{
if (context.Resource is HttpContext httpContext)
{
if (httpContext.Request.Headers.TryGetValue("X-API-KEY", out var header))
{
var val = header.FirstOrDefault()?.ToLower();
if (val == "my-super-secret-key")
{
return Task.FromResult(true);
}
}
}
return Task.FromResult(false);
}));
});
I have decorated an API with this:
[HttpDelete("{itemId:guid}")]
[Authorize(Policy = Auth.Constants.WebmasterPolicyName)]
public async Task<ActionResult> DeleteCatalogItemAsync(Guid itemId)
This works perfectly, when I set the correct API key in the request.
The problem is the negative case: When the key is missing or wrong, I will get a 500 error:
System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).
at Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext context, String scheme, AuthenticationProperties properties)
at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at AlwaysOn.CatalogService.Startup.<>c__DisplayClass5_0.<<Configure>b__3>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
But I'm not sure what to do with that message. I would just like it to return a 401 response to the client.
We can create a custom ApiKeyMiddleware to implemente simple API key authentication.
It is somehow similar to what we have done in the custom attribute, but the main difference that you will notice here is that we cannot directly set the Response object of the context but we have to assign the statuscode and message separately.
Sample Code:
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;
namespace SecuringWebApiUsingApiKey.Middleware
{
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private const string APIKEYNAME = "ApiKey";
public ApiKeyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Headers.TryGetValue(APIKEYNAME, out var extractedApiKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Api Key was not provided. (Using ApiKeyMiddleware) ");
return;
}
var appSettings = context.RequestServices.GetRequiredService<IConfiguration>();
var apiKey = appSettings.GetValue<string>(APIKEYNAME);
if (!apiKey.Equals(extractedApiKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized client. (Using ApiKeyMiddleware)");
return;
}
await _next(context);
}
}
}
For more details, we can refer this blog.
Secure ASP.NET Core Web API using API Key Authentication
You need to implement a custom authentication handler in order to properly handle this. Here is a good example on how to do it.
To summarize the idea, you need to register a custom authentication scheme:
builder.Services.AddAuthentication("ApiKey")
.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationSchemeHandler>(
"ApiKey",
opts => opts.ApiKey = configuration.GetValue<string>("api-key")
);
And define a class for options:
public class ApiKeyAuthenticationSchemeOptions: AuthenticationSchemeOptions {
public string ApiKey {get; set;}
}
And implement a handler class ApiKeyAuthenticationSchemeHandler:AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>.
With a method
protected override Task<AuthenticateResult> HandleAuthenticateAsync() {
var apiKey = Context.Request.Headers["X-API-KEY"];
if (apiKey != Options.ApiKey) {
return Task.FromResult(AuthenticateResult.Fail("Invalid X-API-KEY"));
}
var claims = new[] { new Claim(ClaimTypes.Name, "VALID USER") };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
And last, in your controller you can add an Authorize attibute, like this:
[Authorize(AuthenticationSchemes = "ApiKey")]
I am validating a token stored on cookies, so I created a class
public class VIEWAuthorizeAttribute : AuthorizeAttribute
then I overrode the OnAuthorization class
public override void OnAuthorization(AuthorizationContext filterContext)
{
var cookie = filterContext.HttpContext.Request.Cookies.Get("Profile"); //This is working
if (cookie != null && IsValidToken(cookie["Token"]))
{
return;
}
HandleUnauthorizedRequest(filterContext);
}
That works fine for MVC Controllers, but when I try to do something similar for web api controller, I am not able to get the cookies from the request.
public override void OnAuthorization(HttpActionContext actionContext)
{
var foo = actionContext.Request.Headers.Cookies; //that is not working
if (Authorize(actionContext))
{
return;
}
HandleUnauthorizedRequest(actionContext);
}
actionContext.Request.Headers does not have a method Cookies, I also tried with actionContext.Request.Headers.GetCookies("Bar") like this answer, but Header does not have that GetCookies method.
Any idea?
string Authentication = string.Empty;
if (actionContext.Request.Headers.Contains("Cookie_Phone"))
{
Authentication = actionContext.Request.Headers.GetValues("Cookie_Phone")?.FirstOrDefault();
}
To use this extension method the namespace System.Net.Http is needed.
Also, pay attention on the GetCookies(string name) is an extension method that returns any cookie header present in the request that contain a cookie state whose name that matches the specified value. It returns a CookieHeaderValue that contains a collection of CookieValueCollection instances. So you need extra enumeration to get a CookieState by name that contains a value.
Here is the valid code:
public async Task AuthenticateAsync(
HttpAuthenticationContext context,
CancellationToken cancellationToken)
{
var request = context.Request;
var token = request.Headers
.GetCookies("token").FirstOrDefault()
?.Cookies.FirstOrDefault(x=>x.Name == "token")
?.Value;
if (string.IsNullOrEmpty(token))
{
context.ErrorResult = new AuthenticationFailureResult(
"The token is missing.",
request);
return;
}
var principal = await SomeAuthentication(token);
if (principal == null)
{
context.ErrorResult = new AuthenticationFailureResult(
"The principal has not been resolved.",
request);
return;
}
context.Principal = principal;
}
Writing code for controllers could lead to repeat myself again and again.
How can reuse the code below and apply DRY principle on C# Net Core 2.0. MVC controllers?
See the below example.
The coding for getting a full list of departments using EF and web API is as follows..
[HttpGet]
public async Task<IActionResult> Department()
{
using (var client = await _apiHttpClient.GetHttpClientAsync())
{
var response = await client.GetAsync("api/Department");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var dptos = JsonConvert.DeserializeObject<Department[]>(content);
return View(dptos);
}
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
return RedirectToAction("AccessDenied", "Authorization");
throw new Exception($"A problem happened while calling the API: {response.ReasonPhrase}");
}
}
Is indeed almost identical to get a single department..
[HttpGet]
public async Task<IActionResult> DeparmentEdit(string id)
{
ViewData["id"] = id;
using (var client = await _apiHttpClient.GetHttpClientAsync())
{
var response = await client.GetAsync($"api/Department/{id}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var dpto = JsonConvert.DeserializeObject<Department>(content);
return View(dpto);
}
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
return RedirectToAction("AccessDenied", "Authorization");
throw new Exception($"A problem happened while calling the API: {response.ReasonPhrase}");
}
}
The _apiHttpClient field holds a custom implementation of an HttpClient for tokens and refreshing tokens to access the web API.
I think that IS NOT relevant here to apply refactoring and DRY but anyway I will copy his implementation here below.
BR and thanks in advance for your reply.
public class ApiHttpClient : IApiHttpClient
{
private HttpClient _httpClient;
private HttpClient HttpClient => _httpClient ?? (_httpClient = new HttpClient());
private readonly IHttpContextAccessor _httpContextAccessor;
public ApiHttpClient(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<HttpClient> GetHttpClientAsync()
{
string accessToken;
var context = _httpContextAccessor.HttpContext;
var expiresAt = await context.GetTokenAsync(Constants.Tokens.ExpiresAt); // Get expires_at value
if (string.IsNullOrWhiteSpace(expiresAt) // Should we renew access & refresh tokens?
|| (DateTime.Parse(expiresAt).AddSeconds(-60)).ToUniversalTime() < DateTime.UtcNow) // Make sure to use the exact UTC date formats for comparison
{
accessToken = await RefreshTokensAsync(_httpContextAccessor.HttpContext); // Get the current HttpContext to access the tokens
}
else
{
accessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); // Get access token
}
HttpClient.BaseAddress = new Uri(Constants.Urls.ApiHost);
if (!string.IsNullOrWhiteSpace(accessToken))
HttpClient.SetBearerToken(accessToken);
return HttpClient;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected void Dispose(bool disposing)
{
if (disposing)
{
if (_httpClient != null)
{
_httpClient.Dispose();
_httpClient = null;
}
}
}
public static async Task<string> RefreshTokensAsync(HttpContext context)
{
var discoveryResponse = await DiscoveryClient.GetAsync(Constants.Authority); // Retrive metadata information about our IDP
var tokenClient = new TokenClient(discoveryResponse.TokenEndpoint, Constants.ClientMvc.Id, Constants.ClientMvc.Secret); // Get token client using the token end point. We will use this client to request new tokens later on
var refreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); // Get the current refresh token
var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken); // We request a new pair of access and refresh tokens using the current refresh token
if (tokenResponse.IsError)
return null; // Let's the unauthorized page bubbles up
// throw new Exception("Problem encountered while refreshing tokens", tokenResponse.Exception);
var expiresAt = (DateTime.UtcNow
+ TimeSpan.FromSeconds(tokenResponse.ExpiresIn)).ToString("O", CultureInfo.InvariantCulture); // New expires_at token ISO 860
var authenticateResult = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); // HttpContext.Authentication.GetAuthenticateInfoAsync() deprecated
authenticateResult.Properties.UpdateTokenValue(OpenIdConnectParameterNames.AccessToken, tokenResponse.AccessToken); // New access_token
authenticateResult.Properties.UpdateTokenValue(OpenIdConnectParameterNames.RefreshToken, tokenResponse.RefreshToken); // New refresh_token
authenticateResult.Properties.UpdateTokenValue(Constants.Tokens.ExpiresAt, expiresAt); // New expires_at token ISO 8601 WHY _at TODO
await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, authenticateResult.Principal, authenticateResult.Properties); // Signing in again with the new values, doing such a user relogin, ensuring that we change the cookies on client side. Doig so the user that has logged in has the refreshed tokens
return tokenResponse.AccessToken;
}
public static async Task RevokeTokensAsync(HttpContext context)
{
var discoveryResponse = await DiscoveryClient.GetAsync(Constants.Authority); // Retrive metadata information about our IDP
var revocationClient = new TokenRevocationClient(discoveryResponse.RevocationEndpoint, Constants.ClientMvc.Id, Constants.ClientMvc.Secret); // Get token revocation client using the token revocation endpoint. We will use this client to revoke tokens later on
var accessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); // Get the access token token to revoke
if (!string.IsNullOrWhiteSpace(accessToken))
{
var revokeAccessTokenTokenResponse = await revocationClient.RevokeAccessTokenAsync(accessToken);
if (revokeAccessTokenTokenResponse.IsError)
throw new Exception("Problem encountered while revoking the access token.", revokeAccessTokenTokenResponse.Exception);
}
var refreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); // Get the refresh token to revoke
if (!string.IsNullOrWhiteSpace(refreshToken))
{
var revokeRefreshTokenResponse = await revocationClient.RevokeRefreshTokenAsync(refreshToken);
if (revokeRefreshTokenResponse.IsError)
throw new Exception("Problem encountered while revoking the refresh token.", revokeRefreshTokenResponse.Exception);
}
}
}
I had refactored the code as follows having in mind the following workflow.
We will need: a) an API service class, b) a HttpContextAccessor and c) a HttpClient.
1) DI principle!. We register them in our dependency injection container at ConfigureServices
services
.AddTransient<IGameApiService, GameApiService>()
.AddSingleton<IHttpContextAccessor, HttpContextAccessor>()
.AddSingleton(c => new HttpClient { BaseAddress = new Uri(Constants.Urls.ApiHost) });
2) The big job!. The new GameApiService will do the "heavy job" of calling our API methods. We will call the API using a "composed" request string. The API service will use our HttpClient, passing our request string and returning the response code and A STRING! (instead of using generics or other object) with the content. (I would need help on moving to generic since I fear that the registration on the dependency container will be "hard" to do with generics).
(the HttpContextAccessor is used for some token methods)
public class GameApiService : IGameApiService
{
private readonly HttpClient _httpClient;
private readonly HttpContext _httpContext;
public GameApiService(HttpClient httpClient, IHttpContextAccessor httpContextAccessor)
{
_httpClient = httpClient;
_httpContext = httpContextAccessor.HttpContext;
_httpClient.AddBearerToken(_httpContext); // Add current access token to the authorization header
}
public async Task<(HttpResponseMessage response, string content)> GetDepartments()
{
return await GetAsync(Constants.EndPoints.GameApi.Department); // "api/Department"
}
public async Task<(HttpResponseMessage response, string content)> GetDepartmenById(string id)
{
return await GetAsync($"{Constants.EndPoints.GameApi.Department}/{id}"); // "api/Department/id"
}
private async Task<(HttpResponseMessage response, string content)> GetAsync(string request)
{
string content = null;
var expiresAt = await _httpContext.GetTokenAsync(Constants.Tokens.ExpiresAt); // Get expires_at value
if (string.IsNullOrWhiteSpace(expiresAt) // Should we renew access & refresh tokens?
|| (DateTime.Parse(expiresAt).AddSeconds(-60)).ToUniversalTime() < DateTime.UtcNow) // Make sure to use the exact UTC date formats for comparison
{
var accessToken = await _httpClient.RefreshTokensAsync(_httpContext); // Try to ge a new access token
if (!string.IsNullOrWhiteSpace(accessToken)) // If succeded set add the new access token to the authorization header
_httpClient.AddBearerToken(_httpContext);
}
var response = await _httpClient.GetAsync(request);
if (response.IsSuccessStatusCode)
{
content = await response.Content.ReadAsStringAsync();
}
else if (response.StatusCode != HttpStatusCode.Unauthorized && response.StatusCode != HttpStatusCode.Forbidden)
{
throw new Exception($"A problem happened while calling the API: {response.ReasonPhrase}");
}
return (response, content);
}
}
public interface IGameApiService
{
Task<(HttpResponseMessage response, string content)> GetDepartments();
Task<(HttpResponseMessage response, string content)> GetDepartmenById(string id);
}
3) Great DRY! Our MVC controller will use this new API service as follows.. (we really don't have very much code there and THIS IS THE GOAL.. ;-) GREAT!!.
We still keep the responsibility of de-serialize the content string on the controller action on which the service API method was invoked. The code for the service API looks like...
[Route("[controller]/[action]")]
public class DepartmentController : Controller
{
private readonly IGameApiService _apiService;
public DepartmentController(IGameApiService apiService)
{
_apiService = apiService;
}
[HttpGet]
public async Task<IActionResult> Department()
{
ViewData["Name"] = User.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Name)?.Value;
var (response, content) = await _apiService.GetDepartments();
if (!response.IsSuccessStatusCode) return Forbid();
return View(JsonConvert.DeserializeObject<Department[]>(content));
}
[HttpGet]
public async Task<IActionResult> DepartmentEdit(string id)
{
ViewData["id"] = id;
var (response, content) = await _apiService.GetDepartmenById(id);
if (!response.IsSuccessStatusCode) return Forbid();
return View(JsonConvert.DeserializeObject<Department>(content));
}
}
4) Last trick!. To redirect to a custom page when we are not authorized or the permission has been denied we have issued if (!response.IsSuccessStatusCode) return Forbid(); yes Forbid(). But we still need to configure the default denied page on the cookie middleware. Thus on ConfigureServices we do it with services.AddAuthentication().AddCookie(AddCookie) methods, configuring the relevant options, mainly the AccessDeniedPath option as follows.
private static void AddCookie(CookieAuthenticationOptions options)
{
options.Cookie.Name = "mgame";
options.AccessDeniedPath = "/Authorization/AccessDenied"; // Redirect to custom access denied page when user get access is denied
options.Cookie.HttpOnly = true; // Prevent cookies from being accessed by malicius javascript code
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // Cookie only will be sent over https
options.ExpireTimeSpan = TimeSpan.FromMinutes(Constants.CookieTokenExpireTimeSpan); // Cookie will expire automaticaly after being created and the client will redirect back to Identity Server
}
5) A word about the HTTP Client!. It will be instantiated using a factory on the dependency injection. A new instance is created per GameApiService instance.
The helper code to set the bearer token on the header and refresh the access token has been moved to a convenient extension method helper class as follows..
public static class HttpClientExtensions
{
public static async void AddBearerToken(this HttpClient client, HttpContext context)
{
var accessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
if (!string.IsNullOrWhiteSpace(accessToken))
client.SetBearerToken(accessToken);
}
public static async Task<string> RefreshTokensAsync(this HttpClient client, HttpContext context)
{
var discoveryResponse = await DiscoveryClient.GetAsync(Constants.Authority); // Retrive metadata information about our IDP
var tokenClient = new TokenClient(discoveryResponse.TokenEndpoint, Constants.ClientMvc.Id, Constants.ClientMvc.Secret); // Get token client using the token end point. We will use this client to request new tokens later on
var refreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); // Get the current refresh token
var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken); // We request a new pair of access and refresh tokens using the current refresh token
if (tokenResponse.IsError) // Let's the unauthorized page bubbles up instead doing throw new Exception("Problem encountered while refreshing tokens", tokenResponse.Exception)
return null;
var expiresAt = (DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn)).ToString("O", CultureInfo.InvariantCulture); // New expires_at token ISO 860
var authenticateResult = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); // HttpContext.Authentication.GetAuthenticateInfoAsync() deprecated
authenticateResult.Properties.UpdateTokenValue(OpenIdConnectParameterNames.AccessToken, tokenResponse.AccessToken); // New access_token
authenticateResult.Properties.UpdateTokenValue(OpenIdConnectParameterNames.RefreshToken, tokenResponse.RefreshToken); // New refresh_token
authenticateResult.Properties.UpdateTokenValue(Constants.Tokens.ExpiresAt, expiresAt); // New expires_at token ISO 8601
await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, authenticateResult.Principal, authenticateResult.Properties); // Signing in again with the new values, doing such a user relogin, ensuring that we change the cookies on client side. Doig so the user that has logged in has the refreshed tokens
return tokenResponse.AccessToken;
}
public static async Task RevokeTokensAsync(this HttpClient client, HttpContext context)
{
var discoveryResponse = await DiscoveryClient.GetAsync(Constants.Authority); // Retrive metadata information about our IDP
var revocationClient = new TokenRevocationClient(discoveryResponse.RevocationEndpoint, Constants.ClientMvc.Id, Constants.ClientMvc.Secret); // Get token revocation client using the token revocation endpoint. We will use this client to revoke tokens later on
var accessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); // Get the access token token to revoke
if (!string.IsNullOrWhiteSpace(accessToken))
{
var revokeAccessTokenTokenResponse = await revocationClient.RevokeAccessTokenAsync(accessToken);
if (revokeAccessTokenTokenResponse.IsError)
throw new Exception("Problem encountered while revoking the access token.", revokeAccessTokenTokenResponse.Exception);
}
var refreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); // Get the refresh token to revoke
if (!string.IsNullOrWhiteSpace(refreshToken))
{
var revokeRefreshTokenResponse = await revocationClient.RevokeRefreshTokenAsync(refreshToken);
if (revokeRefreshTokenResponse.IsError)
throw new Exception("Problem encountered while revoking the refresh token.", revokeRefreshTokenResponse.Exception);
}
}
}
Now the code after refactoring it looks more pretty and clean.. ;-)
You could just split it up using generics. I haven't debugged this code (obviously), but I think it gets you where you need to go.
using System.Security.Authentication;
[HttpGet]
public async Task<IActionResult> Department() {
try {
var myObject = await GetSafeData<Department[]>("api/Department");
return view(myObj);
} catch(AuthenticationException ex) {
return RedirectToAction("AccessDenied", "Authorization");
}
}
internal T GetSafeData<T>(string url) {
using (var client = await _apiHttpClient.GetHttpClientAsync()) {
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode) {
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(content);
}
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
Throw New AuthenticationException("");
throw new Exception($"A problem happened while calling the API: {response.ReasonPhrase}");
}
}
You can sorta see how you might pass response to that same method, so you could do your AccessDenied redirect within that method as well and reduce your repetitive code everywhere.
It's a generic method, so you can use it for ANY call to that api. That should be enough to get you started. Hope it helps!
In my Web API, I want to get the access token from the Cookies header in the request, and then do the validation on the token. At the moment, IdentityServer3.AccessTokenValidation package is used to do the validation of the Bearer token and it looks for the token from the Authorization header only. Preferably I would like to keep using the same bearer token validation process, but getting the token from the Cookies header, does that sound doable with handy codes? Thanks
Just implement your own TokenProvider and provide it to the AccessTokenValidationMiddleware:
public class MyCustomTokenProvider : IOAuthBearerAuthenticationProvider
{
public Task RequestToken(OAuthRequestTokenContext context)
{
if (context.Token == null)
{
//try get from cookie
var tokenCookie = context.Request.Cookies["myCookieName"];
if (tokenCookie != null)
{
context.Token = tokenCookie;
}
}
return Task.FromResult(0);
}
public Task ValidateIdentity(OAuthValidateIdentityContext context)
{
throw new NotImplementedException();
}
public Task ApplyChallenge(OAuthChallengeContext context)
{
throw new NotImplementedException();
}
}
In your Startup.cs:
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://myhost",
RequiredScopes = new[] { "my-scope" },
TokenProvider = new MyCustomTokenProvider()
});
I am using Web API 2 with OWIN token based authentication. Only thing that is not working is authorization based on roles.
In my implementation of AuthorizationServerProvider.GrantResourceOwnerCredentials this is how i assign roles:
identity.AddClaim(client.ApplicationType == ApplicationTypes.WebClient
? new Claim(ClaimTypes.Role, "user")
: new Claim(ClaimTypes.Role, "admin"));
But in the Controller using [Authenticate(Roles="user")] just returns a Authorization denied message to the client. I checked the variables and this is whats inside
So the role seems to be there, but user.Claims is empty and IsInRole("user") also returns negative.
I found several questions here on stackoverflow and logic-wise i don't see what i missed. Only thing that comes to my mind is overwriting the Authorize Command but this is kind of needless as role based authorization seems to be integrated already...
EDIT: This is what my workig method looks like:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin") ?? "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
Client client;
using (var repo = new AuthRepository())
{
client = repo.FindClient(context.ClientId);
if (client.ApplicationType != ApplicationTypes.Service)
{
var user = await repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect." + context.UserName);
return;
}
}
}
Don't add the role claim directly, use the UserManager instead:
UserManagerInstance.AddToRole(userId, "admin");
That way the role will be persisted (to the AspNetUserRoles or whatever you have configured) so it will be there for the later requests. This doesn't happen if you add the claim directly, since you're adding it to an "instance" of your user identity that will die with the current request.
TO ANSWER YOUR FURTHER REQUIREMENTS:
If you want the claims codified on the ticket then you have to do this after adding the claims the way you're doing (in GrantResourceOwnerCredentials):
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{ "userId", "blah,blah" },
{ "role", "admin" }
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
This way you don't have to "persist" these kind of users
Of course you would have to override the TokenEndpoint method of the OAuthAuthorizationServerProvider in order to retrive those data on later request/responses.
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
Probably solved it somehow, but for me it works if I put it like this:
[Authorize(Roles = "user")]
[Route("")]
[HttpGet]
public async Task<IHttpActionResult> GetUserSpecificServers() { ... }