AspNet.Security.OpenIdConnect vs OAuthAuthorizationProvider - asp.net

I had an app in .NET Framework in which I implemented OAuthAuthorizationServer. Now I want to upgrade my app to .NET Core 2.1, so I did some R&D and decided to use ASOS. Now the issue is I have implemented ASOS and it is working fine but I have some chunks that I can't figure out how to convert.
private Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(context.UserName, OAuthDefaults.AuthenticationType),
context.Scope.Select(x => new Claim("claim", x)));
context.Validated(identity);
return Task.FromResult(0);
}
private Task GrantClientCredetails(OAuthGrantClientCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(context.ClientId, OAuthDefaults.AuthenticationType),
context.Scope.Select(x => new Claim("claim", x)));
context.Validated(identity);
return Task.FromResult(0);
}
private readonly ConcurrentDictionary<string, string> _authenticationCodes =
new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
private void CreateAuthenticationCode(AuthenticationTokenCreateContext context)
{
context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
_authenticationCodes[context.Token] = context.SerializeTicket();
}
private void ReceiveAuthenticationCode(AuthenticationTokenReceiveContext context)
{
string value;
if (_authenticationCodes.TryRemove(context.Token, out value))
{
context.DeserializeTicket(value);
}
}
private void CreateRefreshToken(AuthenticationTokenCreateContext context)
{
context.SetToken(context.SerializeTicket());
}
private void ReceiveRefreshToken(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
}
Now I have couple of question:
Client Credentials and Resource owner password grant types are two different grant types so how can we differentiate in them using ASOS?
GrantResourceOwnerCredentials takes OAuthGrantResourceOwnerCredentialsContext as a param and GrantClientCredentials takes OAuthGrantClientCredentialsContext as a param. Both these contexts contains scope which is not available in ASOS.
How can I serialize and deserialize access and refresh tokens like I was doing OAuthAuthorizationProvider?
How do we handle refresh tokens in ASOS? I can see refresh tokens in response but I haven't write any logic for refresh token my self.

Client Credentials and Resource owner password grant types are two different grant types so how can we differentiate in them using ASOS?
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
if (context.Request.IsClientCredentialsGrantType())
{
// ...
}
else if (context.Request.IsPasswordGrantType())
{
// ...
}
else
{
throw new NotSupportedException();
}
}
GrantResourceOwnerCredentials takes OAuthGrantResourceOwnerCredentialsContext as a param and GrantClientCredetails takes OAuthGrantClientCredentialsContext as a param. Both these contexts contains scope which is not available in ASOS
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
var scopes = context.Request.GetScopes();
// ...
}
How can I serialize and deserialize access and refresh tokens like I was doing OAuthAUthorizationProvider?
By using the OnSerializeAccessToken/OnDeserializeAccessToken and OnSerializeRefreshToken/OnDeserializeRefreshToken events.
How do we handle refresh tokens in ASOS? I can see refresh tokens in response but I haven't write any logic for refresh token my self.
Unlike Katana's OAuth server middleware, ASOS provides default logic for generating authorization codes and refresh tokens. If you want to use implement things like token revocation, you can do that in the events I mentioned. Read AspNet.Security.OpenIdConnect.Server. Refresh tokens for more information.
Here's an example that returns GUID refresh tokens and stores the associated (encrypted) payload in a database:
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace AuthorizationServer
{
public class MyToken
{
public string Id { get; set; }
public string Payload { get; set; }
}
public class MyDbContext : DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options)
: base(options) { }
public DbSet<MyToken> Tokens { get; set; }
}
public class MyProvider : OpenIdConnectServerProvider
{
private readonly MyDbContext _database;
public MyProvider(MyDbContext database)
{
_database = database;
}
public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
{
if (!context.Request.IsPasswordGrantType() && !context.Request.IsRefreshTokenGrantType())
{
context.Reject(error: OpenIdConnectConstants.Errors.UnsupportedGrantType);
}
else
{
// Don't enforce client authentication.
context.Skip();
}
return Task.CompletedTask;
}
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
if (context.Request.IsPasswordGrantType())
{
if (context.Request.Username == "bob" && context.Request.Password == "bob")
{
var identity = new ClaimsIdentity(context.Scheme.Name);
identity.AddClaim(new Claim(OpenIdConnectConstants.Claims.Subject, "Bob"));
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), identity.AuthenticationType);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OfflineAccess);
context.Validate(ticket);
}
else
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The username/password couple is invalid.");
}
}
else
{
var token = await _database.Tokens.FindAsync(context.Request.RefreshToken);
_database.Tokens.Remove(token);
await _database.SaveChangesAsync();
context.Validate(context.Ticket);
}
}
public override async Task SerializeRefreshToken(SerializeRefreshTokenContext context)
{
context.RefreshToken = Guid.NewGuid().ToString();
_database.Tokens.Add(new MyToken
{
Id = context.RefreshToken,
Payload = context.Options.RefreshTokenFormat.Protect(context.Ticket)
});
await _database.SaveChangesAsync();
}
public override async Task DeserializeRefreshToken(DeserializeRefreshTokenContext context)
{
context.HandleDeserialization();
var token = await _database.Tokens.FindAsync(context.RefreshToken);
if (token == null)
{
return;
}
context.Ticket = context.Options.RefreshTokenFormat.Unprotect(token.Payload);
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyDbContext>(options =>
{
options.UseInMemoryDatabase(nameof(MyDbContext));
});
services.AddAuthentication()
.AddOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/token";
options.ProviderType = typeof(MyProvider);
options.AllowInsecureHttp = true;
})
.AddOAuthValidation();
services.AddMvc();
services.AddScoped<MyProvider>();
}
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
}
}
}

Related

Validate 3rd Party Cookies with ASP.NET Core Web Api

I am using ORY Kratos for identity and my frontend SPA (React App) is authenticating against the Kratos Login Server and gets a session cookie back.
Now I want to secure my ASP.NET Core Web Api in a way, that a user can only call certain methods protected with the [Authorize] attribute when attaching a valid cookie to the request. For this, I need to validate the cookie from every incoming request. So I am looking for a way to configure Authentication and add custom logic to validate the cookie (I need to make an API call to Kratos to validate it).
The cookie I want to validate has not been issued by the ASP.NET Core App that wants to validate it.
All the samples I found so far, are also issuing the cookie on the same server but I need to validate an external one.
This is what my cookie looks like:
In the Dev Tools, I can validate that the Cookie is attached to the requests header:
This is, what I've tried so far:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = "ory_kratos_session";
options.Cookie.Path = "/";
options.Cookie.Domain = "localhost";
options.Cookie.HttpOnly = true;
options.EventsType = typeof(CustomCookieAuthenticationEvents);
});
services.AddScoped<CustomCookieAuthenticationEvents>();
// ...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseAuthentication();
app.UseAuthorization();
// ...
}
public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
public CustomCookieAuthenticationEvents() {}
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
// Never gets called
}
}
Logs:
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[7]
Cookies was not authenticated. Failure message: Unprotect ticket failed
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[12]
AuthenticationScheme: Cookies was challenged.
dbug: Microsoft.AspNetCore.Server.Kestrel[9]
Connection id "0HM6IBAO4PLLL" completed keep alive response.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished HTTP/1.1 GET https://localhost:5001/weatherforecast - - - 302 0 - 75.3183ms
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET https://localhost:5001/Account/Login?ReturnUrl=%2Fweatherforecast - -
According to the cookie authentication handler's source codes, I found it will read the cookie before goes to the CustomCookieAuthenticationEvents .
Some part of codes as below:
private async Task<AuthenticateResult> ReadCookieTicket()
{
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name!);
if (string.IsNullOrEmpty(cookie))
{
return AuthenticateResult.NoResult();
}
var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
if (ticket == null)
{
return AuthenticateResult.Fail("Unprotect ticket failed");
}
if (Options.SessionStore != null)
{
var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim));
if (claim == null)
{
return AuthenticateResult.Fail("SessionId missing");
}
// Only store _sessionKey if it matches an existing session. Otherwise we'll create a new one.
ticket = await Options.SessionStore.RetrieveAsync(claim.Value);
if (ticket == null)
{
return AuthenticateResult.Fail("Identity missing in session store");
}
_sessionKey = claim.Value;
}
var currentUtc = Clock.UtcNow;
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc != null && expiresUtc.Value < currentUtc)
{
if (Options.SessionStore != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey!);
}
return AuthenticateResult.Fail("Ticket expired");
}
CheckForRefresh(ticket);
// Finally we have a valid ticket
return AuthenticateResult.Success(ticket);
}
If you still want to use cookie authentication, you need to rewrite the handler. So I suggest you could write a custom AuthenticationHandler and AuthenticationSchemeOptions class like below and register the class in the startup.cs directly. Then you could use [Authorize(AuthenticationSchemes = "Test")] to set the special AuthenticationSchemes.
Codes:
public class ValidateHashAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
public class ValidateHashAuthenticationHandler
: AuthenticationHandler<ValidateHashAuthenticationSchemeOptions>
{
public ValidateHashAuthenticationHandler(
IOptionsMonitor<ValidateHashAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
//TokenModel model;
// validation comes in here
if (!Request.Headers.ContainsKey("X-Base-Token"))
{
return Task.FromResult(AuthenticateResult.Fail("Header Not Found."));
}
var token = Request.Headers["X-Base-Token"].ToString();
try
{
// convert the input string into byte stream
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(token)))
{
// deserialize stream into token model object
//model = Serializer.Deserialize<TokenModel>(stream);
}
}
catch (System.Exception ex)
{
Console.WriteLine("Exception Occured while Deserializing: " + ex);
return Task.FromResult(AuthenticateResult.Fail("TokenParseException"));
}
//if (model != null)
//{
// // success case AuthenticationTicket generation
// // happens from here
// // create claims array from the model
// var claims = new[] {
// new Claim(ClaimTypes.NameIdentifier, model.UserId.ToString()),
// new Claim(ClaimTypes.Email, model.EmailAddress),
// new Claim(ClaimTypes.Name, model.Name) };
// // generate claimsIdentity on the name of the class
// var claimsIdentity = new ClaimsIdentity(claims,
// nameof(ValidateHashAuthenticationHandler));
// // generate AuthenticationTicket from the Identity
// // and current authentication scheme
// var ticket = new AuthenticationTicket(
// new ClaimsPrincipal(claimsIdentity), this.Scheme.Name);
// // pass on the ticket to the middleware
// return Task.FromResult(AuthenticateResult.Success(ticket));
//}
return Task.FromResult(AuthenticateResult.Fail("Model is Empty"));
}
}
public class TokenModel
{
public int UserId { get; set; }
public string Name { get; set; }
public string EmailAddress { get; set; }
}
Startup.cs add below codes into the ConfigureServices method:
services.AddAuthentication(options =>
{
options.DefaultScheme
= "Test";
})
.AddScheme<ValidateHashAuthenticationSchemeOptions, ValidateHashAuthenticationHandler>
("Test", null);
Usage:
Controller:
[Authorize(AuthenticationSchemes = "Test")]
So I now solved it for myself differently by creating a custom Authentication Handler, which takes care of checking the Cookie that gets send to the Web API.
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddSingleton(new KratosService("http://localhost:4433"));
services
.AddAuthentication("Kratos")
.AddScheme<KratosAuthenticationOptions, KratosAuthenticationHandler>("Kratos", null);
// ...
}
If you are interested in the implementation that did the job for me, I have attached the additional files below. It is also worth mentioning, that Kratos supports two way of Authenticating: Cookies and Bearer Tokens, depending on if you talk to it via Web App or an API. My implementation supports both. You can find a working Sample with ASP.NET Core and React here: https://github.com/robinmanuelthiel/kratos-demo
KratosAuthenticationHandler.cs:
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace KratosDemo.Server.Kratos
{
public class KratosAuthenticationOptions : AuthenticationSchemeOptions
{
}
public class KratosAuthenticationHandler : AuthenticationHandler<KratosAuthenticationOptions>
{
readonly KratosService _kratosService;
readonly string _sessionCookieName = "ory_kratos_session";
public KratosAuthenticationHandler(
IOptionsMonitor<KratosAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
KratosService kratosService
)
: base(options, logger, encoder, clock)
{
_kratosService = kratosService;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// ORY Kratos can authenticate against an API through two different methods:
// Cookie Authentication is for Browser Clients and sends a Session Cookie with each request.
// Bearer Token Authentication is for Native Apps and other APIs and sends an Authentication header with each request.
// We are validating both ways here by sending a /whoami request to ORY Kratos passing the provided authentication
// methods on to Kratos.
try
{
// Check, if Cookie was set
if (Request.Cookies.ContainsKey(_sessionCookieName))
{
var cookie = Request.Cookies[_sessionCookieName];
var id = await _kratosService.GetUserIdByCookie(_sessionCookieName, cookie);
return ValidateToken(id);
}
// Check, if Authorization header was set
if (Request.Headers.ContainsKey("Authorization"))
{
var token = Request.Headers["Authorization"];
var id = await _kratosService.GetUserIdByToken(token);
return ValidateToken(id);
}
// If neither Cookie nor Authorization header was set, the request can't be authenticated.
return AuthenticateResult.NoResult();
}
catch (Exception ex)
{
// If an error occurs while trying to validate the token, the Authentication request fails.
return AuthenticateResult.Fail(ex.Message);
}
}
private AuthenticateResult ValidateToken(string userId)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userId),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new System.Security.Principal.GenericPrincipal(identity, null);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
}
KratosService.cs:
using System.Threading.Tasks;
using System.Net.Http;
using System.Text.Json;
using System;
namespace KratosDemo.Server.Kratos
{
public class KratosService
{
private readonly string _kratosUrl;
private readonly HttpClient _client;
public KratosService(string kratosUrl)
{
_client = new HttpClient();
_kratosUrl = kratosUrl;
}
public async Task<string> GetUserIdByToken(string token)
{
var request = new HttpRequestMessage(HttpMethod.Get, $"{_kratosUrl}/sessions/whoami");
request.Headers.Add("Authorization", token);
return await SendWhoamiRequestAsync(request);
}
public async Task<string> GetUserIdByCookie(string cookieName, string cookieContent)
{
var request = new HttpRequestMessage(HttpMethod.Get, $"{_kratosUrl}/sessions/whoami");
request.Headers.Add("Cookie", $"{cookieName}={cookieContent}");
return await SendWhoamiRequestAsync(request);
}
private async Task<string> SendWhoamiRequestAsync(HttpRequestMessage request)
{
var res = await _client.SendAsync(request);
res.EnsureSuccessStatusCode();
var json = await res.Content.ReadAsStringAsync();
var whoami = JsonSerializer.Deserialize<Whoami>(json);
if (!whoami.Active)
throw new InvalidOperationException("Session is not active.");
return whoami.Identity.Id;
}
}
}
Whoami.cs:
using System;
using System.Text.Json.Serialization;
namespace KratosDemo.Server.Kratos
{
public class Whoami
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("active")]
public bool Active { get; set; }
[JsonPropertyName("expires_at")]
public DateTime ExpiresAt { get; set; }
[JsonPropertyName("authenticated_at")]
public DateTime AuthenticatedAt { get; set; }
[JsonPropertyName("issued_at")]
public DateTime IssuedAt { get; set; }
[JsonPropertyName("identity")]
public Identity Identity { get; set; }
}
public class Identity
{
[JsonPropertyName("id")]
public string Id { get; set; }
}
}

Secure swagger docs page with username and password Asp Net Core 2.1

I am using Asp.Net Core 2.1 Web Api with Swashbuckle.aspnetcore.swagger
I want to secure api documentation page with username and password before granting access.
Sample documention page
To make sure its not accessible by the public
I have found a solution on GitHub and applied it to my project. its working as expected.
Below code copied from https://github.com/domaindrivendev/Swashbuckle.WebApi/issues/384#issuecomment-410117400
public class SwaggerBasicAuthMiddleware
{
private readonly RequestDelegate next;
public SwaggerBasicAuthMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task InvokeAsync(HttpContext context)
{
//Make sure we are hitting the swagger path, and not doing it locally as it just gets annoying :-)
if (context.Request.Path.StartsWithSegments("/swagger") && !this.IsLocalRequest(context))
{
string authHeader = context.Request.Headers["Authorization"];
if (authHeader != null && authHeader.StartsWith("Basic "))
{
// Get the encoded username and password
var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim();
// Decode from Base64 to string
var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword));
// Split username and password
var username = decodedUsernamePassword.Split(':', 2)[0];
var password = decodedUsernamePassword.Split(':', 2)[1];
// Check if login is correct
if (IsAuthorized(username, password))
{
await next.Invoke(context);
return;
}
}
// Return authentication type (causes browser to show login dialog)
context.Response.Headers["WWW-Authenticate"] = "Basic";
// Return unauthorized
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else
{
await next.Invoke(context);
}
}
public bool IsAuthorized(string username, string password)
{
// Check that username and password are correct
return username.Equals("SpecialUser", StringComparison.InvariantCultureIgnoreCase)
&& password.Equals("SpecialPassword1");
}
public bool IsLocalRequest(HttpContext context)
{
//Handle running using the Microsoft.AspNetCore.TestHost and the site being run entirely locally in memory without an actual TCP/IP connection
if (context.Connection.RemoteIpAddress == null && context.Connection.LocalIpAddress == null)
{
return true;
}
if (context.Connection.RemoteIpAddress.Equals(context.Connection.LocalIpAddress))
{
return true;
}
if (IPAddress.IsLoopback(context.Connection.RemoteIpAddress))
{
return true;
}
return false;
}
}
public static class SwaggerAuthorizeExtensions
{
public static IApplicationBuilder UseSwaggerAuthorized(this IApplicationBuilder builder)
{
return builder.UseMiddleware<SwaggerBasicAuthMiddleware>();
}
}
In Startup.cs
app.UseAuthentication(); //Ensure this like is above the swagger stuff
app.UseSwaggerAuthorized();
app.UseSwagger();
app.UseSwaggerUI();
Copied from mguinness's answer on Github:
In .NET Core you use middleware, instead of a DelegatingHandler:
public class SwaggerAuthorizedMiddleware
{
private readonly RequestDelegate _next;
public SwaggerAuthorizedMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/swagger")
&& !context.User.Identity.IsAuthenticated)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
await _next.Invoke(context);
}
}
You will also need an extension method to help adding to pipeline:
public static class SwaggerAuthorizeExtensions
{
public static IApplicationBuilder UseSwaggerAuthorized(this IApplicationBuilder builder)
{
return builder.UseMiddleware<SwaggerAuthorizedMiddleware>();
}
}
Then add to Configure method in Startup.cs just before using Swagger:
app.UseSwaggerAuthorized();
app.UseSwagger();
app.UseSwaggerUi();
There's also a variant solution posted there how to do it with basic auth.

.NET Core API + MVC Core Client supported by RestSharp

I have created .NET Core Web Api with JWT authentication. Now, I am in the middle of creating web app using MVC Core. In MVC project I have API client wrapper:
Interface:
public interface IWebApiService
{
Task<T> AuthenticateAsync<T>(string userName);
Task<T> GetAsync<T>(string action, string authToken);
Task PutAsync<T>(string action, T data, string authToken);
Task PostAsync<T>(string action, T data, string authToken);
}
Implementation:
public class WebApiService : IWebApiService
{
private readonly WebApiSettings _webApiSettings;
public WebApiService(WebApiSettings webApiSettings)
{
_webApiSettings = webApiSettings;
}
public async Task<T> AuthenticateAsync<T>(string userName)
{
var client = new RestClient(_webApiSettings.BaseUri);
var request = new RestRequest("/Login", Method.POST)
{
RequestFormat = DataFormat.Json
};
request.AddBody(new { UserName = userName });
var response = await client.ExecuteTaskAsync(request);
if (response.IsSuccessful)
{
return JsonConvert.DeserializeObject<T>(response.Content);
}
throw new ApiException(response.StatusCode.ToString(), response.ErrorMessage);
}
public async Task<T> GetAsync<T>(string action, string authToken)
{
var client = new RestClient(_webApiSettings.BaseUri);
var request = new RestRequest(action, Method.GET)
{
RequestFormat = DataFormat.Json
};
request.AddHeader("Authorization", $"Bearer {authToken}");
var response = await client.ExecuteTaskAsync(request);
if (response.IsSuccessful)
{
return JsonConvert.DeserializeObject<T>(response.Content);
}
throw new ApiException(response.StatusCode.ToString(), response.ErrorMessage);
}
public Task PutAsync<T>(string action, T data, string authToken)
{
// TODO
throw new NotImplementedException();
}
public Task PostAsync<T>(string action, T data, string authToken)
{
// TODO
throw new NotImplementedException();
}
}
MVC Login Controller:
public class LoginController : Controller
{
private readonly IWebApiService _webApiService;
public LoginController(IWebApiService webApiService)
{
_webApiService = webApiService;
}
public async Task<IActionResult> Get(string redirectUrl)
{
var user = User.Identity.Name;
if(user == null)
throw new WebInterfaceException("Invalid username.");
var response = await _webApiService.AuthenticateAsync<JwtToken>(user);
HttpContext.Session.SetObjectAsJson("Token", response);
return Redirect(redirectUrl ?? "/Home/Index");
}
}
I keep JWT object in session as I didn't find better solution for storing tokens in MVC Core.
Below example controller:
public class ExampleController : Controller
{
private readonly IWebApiService _webApiService;
public ExampleController(IWebApiService webApiService)
{
_webApiService = webApiService;
}
[HttpGet]
public async Task<IActionResult> Browse()
{
var jwtToken = HttpContext.Session.GetObjectFromJson<JwtToken>("Token");
if (jwtToken == null)
{
return RedirectToAction("Get", "Login", new { redirectUrl = Request.Path});
}
var response = await _webApiService.GetAsync<IEnumerable<ExampleBrowseViewModel>>("/Examples", jwtToken.Token);
return Json(response);
}
}
My problem is that in every controller action I will have to check if token is not null. If it's null, I am redirecting to Login page where I am retrieving token from API and redirecting to originally requested page. I would like to have some token handler where so I will not repeat the same code over and over. Additionally in my JWT object I have token expiration time and I would like to refresh it once it will expire so user could continue sending requests to API.
Can you give me few advises so I could accomplish this?

ASP.NET Core 2.0 Bearer Auth without Identity

I thought I had a pretty simple goal in mind when I set out a day ago to implement a self-contained bearer auth webapi on .NET core 2.0, but I have yet to get anything remotely working. Here's a list of what I'm trying to do:
Implement a bearer token protected webapi
Issue tokens & refresh tokens from an endpoint in the same project
Use the [Authorize] attribute to control access to api surface
Not use ASP.Net Identity (I have much lighter weight user/membership reqs)
I'm totally fine with building identity/claims/principal in login and adding that to request context, but I've not seen a single example on how to issue and consume auth/refresh tokens in a Core 2.0 webapi without Identity. I've seen the 1.x MSDN example of cookies without Identity, but that didn't get me far enough in understanding to meet the requirements above.
I feel like this might be a common scenario and it shouldn't be this hard (maybe it's not, maybe just lack of documentation/examples?). As far as I can tell, IdentityServer4 is not compatible with Core 2.0 Auth, opendiddict seems to require Identity. I also don't want to host the token endpoint in a separate process, but within the same webapi instance.
Can anyone point me to a concrete example, or at least give some guidance as to what best steps/options are?
Did an edit to make it compatible with ASP.NET Core 2.0.
Firstly, some Nuget packages:
Microsoft.AspNetCore.Authentication.JwtBearer
Microsoft.AspNetCore.Identity
System.IdentityModel.Tokens.Jwt
System.Security.Cryptography.Csp
Then some basic data transfer objects.
// Presumably you will have an equivalent user account class with a user name.
public class User
{
public string UserName { get; set; }
}
public class JsonWebToken
{
public string access_token { get; set; }
public string token_type { get; set; } = "bearer";
public int expires_in { get; set; }
public string refresh_token { get; set; }
}
Getting into the proper functionality, you'll need a login/token web method to actually send the authorization token to the user.
[Route("api/token")]
public class TokenController : Controller
{
private ITokenProvider _tokenProvider;
public TokenController(ITokenProvider tokenProvider) // We'll create this later, don't worry.
{
_tokenProvider = tokenProvider;
}
public JsonWebToken Get([FromQuery] string grant_type, [FromQuery] string username, [FromQuery] string password, [FromQuery] string refresh_token)
{
// Authenticate depending on the grant type.
User user = grant_type == "refresh_token" ? GetUserByToken(refresh_token) : GetUserByCredentials(username, password);
if (user == null)
throw new UnauthorizedAccessException("No!");
int ageInMinutes = 20; // However long you want...
DateTime expiry = DateTime.UtcNow.AddMinutes(ageInMinutes);
var token = new JsonWebToken {
access_token = _tokenProvider.CreateToken(user, expiry),
expires_in = ageInMinutes * 60
};
if (grant_type != "refresh_token")
token.refresh_token = GenerateRefreshToken(user);
return token;
}
private User GetUserByToken(string refreshToken)
{
// TODO: Check token against your database.
if (refreshToken == "test")
return new User { UserName = "test" };
return null;
}
private User GetUserByCredentials(string username, string password)
{
// TODO: Check username/password against your database.
if (username == password)
return new User { UserName = username };
return null;
}
private string GenerateRefreshToken(User user)
{
// TODO: Create and persist a refresh token.
return "test";
}
}
You probably noticed the token creation is still just "magic" passed through by some imaginary ITokenProvider. Define the token provider interface.
public interface ITokenProvider
{
string CreateToken(User user, DateTime expiry);
// TokenValidationParameters is from Microsoft.IdentityModel.Tokens
TokenValidationParameters GetValidationParameters();
}
I implemented the token creation with an RSA security key on a JWT. So...
public class RsaJwtTokenProvider : ITokenProvider
{
private RsaSecurityKey _key;
private string _algorithm;
private string _issuer;
private string _audience;
public RsaJwtTokenProvider(string issuer, string audience, string keyName)
{
var parameters = new CspParameters { KeyContainerName = keyName };
var provider = new RSACryptoServiceProvider(2048, parameters);
_key = new RsaSecurityKey(provider);
_algorithm = SecurityAlgorithms.RsaSha256Signature;
_issuer = issuer;
_audience = audience;
}
public string CreateToken(User user, DateTime expiry)
{
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user.UserName, "jwt"));
// TODO: Add whatever claims the user may have...
SecurityToken token = tokenHandler.CreateJwtSecurityToken(new SecurityTokenDescriptor
{
Audience = _audience,
Issuer = _issuer,
SigningCredentials = new SigningCredentials(_key, _algorithm),
Expires = expiry.ToUniversalTime(),
Subject = identity
});
return tokenHandler.WriteToken(token);
}
public TokenValidationParameters GetValidationParameters()
{
return new TokenValidationParameters
{
IssuerSigningKey = _key,
ValidAudience = _audience,
ValidIssuer = _issuer,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(0) // Identity and resource servers are the same.
};
}
}
So you're now generating tokens. Time to actually validate them and wire it up. Go to your Startup.cs.
In ConfigureServices()
var tokenProvider = new RsaJwtTokenProvider("issuer", "audience", "mykeyname");
services.AddSingleton<ITokenProvider>(tokenProvider);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = tokenProvider.GetValidationParameters();
});
// This is for the [Authorize] attributes.
services.AddAuthorization(auth => {
auth.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
Then Configure()
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
// Whatever else you're putting in here...
app.UseMvc();
}
That should be about all you need. Hopefully I haven't missed anything.
The happy result is...
[Authorize] // Yay!
[Route("api/values")]
public class ValuesController : Controller
{
// ...
}
Following on #Mitch answer: Auth stack changed quite a bit moving to .NET Core 2.0. Answer below is just using the new implementation.
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
namespace JwtWithoutIdentity
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = "me",
ValidAudience = "you",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")) //Secret
};
});
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseMvc();
}
}
}
Token Controller
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using JwtWithoutIdentity.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
namespace JwtWithoutIdentity.Controllers
{
public class TokenController : Controller
{
[AllowAnonymous]
[Route("api/token")]
[HttpPost]
public async Task<IActionResult> Token(LoginViewModel model)
{
if (!ModelState.IsValid) return BadRequest("Token failed to generate");
var user = (model.Password == "password" && model.Username == "username");
if (!user) return Unauthorized();
//Add Claims
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.UniqueName, "data"),
new Claim(JwtRegisteredClaimNames.Sub, "data"),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")); //Secret
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken("me",
"you",
claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return Ok(new JsonWebToken()
{
access_token = new JwtSecurityTokenHandler().WriteToken(token),
expires_in = 600000,
token_type = "bearer"
});
}
}
}
Values Controller
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace JwtWithoutIdentity.Controllers
{
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET api/values
[Authorize]
[HttpGet]
public IEnumerable<string> Get()
{
var name = User.Identity.Name;
var claims = User.Claims;
return new string[] { "value1", "value2" };
}
}
}
Hope this helps!

ASP.NET Core policy base Authorize with RequireUser with string array

I am creating policy base authorization and would like to allow multiple users in one policy to access webpage.
I created policy like shown below in start up file. Question, How can I use multiple usernames in one policy? I looked at the method for.RequireUserName, it is only accepting string username.
Policy name AdminServiceAccount is mostly I am interested in to add multiple users. If I use param .RequireUserName("DOMAIN\\USER1,DOMAIN\\USER2") will it work? I don't think so, but wanted to check if there is an alternative way.
services.AddAuthorization(
option =>
{
option.AddPolicy("Admin", policy => policy.RequireRole("Domain\\GroupName"));
option.AddPolicy("SuperAdminUser", policy => policy.RequireUserName("DOMAIN\\SuperAdminUser"));
option.AddPolicy("AdminServiceAccount", policy => policy.RequireUserName("DOMAIN\\USER1"));
}
);
UPDATE 1:
UPDATE 2:
So in my Controller, I added [Authorize(Policy = "UserNamesPolicy")] as show below:
[Authorize(Policy = "UserNamesPolicy")]
public class ServersController : Controller
{
private readonly ServerMatrixDbContext _context;
public ServersController(ServerMatrixDbContext context)
{
_context = context;
}
// GET: Servers
public async Task<IActionResult> Index()
{
// Some code here
return View();
}
}
Here is my startup file:
services.AddAuthorization(
option =>
{
option.AddPolicy("UserNamesPolicy",
policy => policy.Requirements.Add(new UserNamesRequirement("DOMAIN\\USER1", "DOMAIN\\USER2"))
);
}
);
services.AddSingleton<IAuthorizationHandler, UserNamesRequirement();
For .AddSingleTon in startup file I get below error:
Here is the handler class:
public class UserNamesHandler : AuthorizationHandler<UserNamesRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserNamesRequirement requirement)
{
var userName = context.User.FindFirst(ClaimTypes.NameIdentifier).Value;
if (requirement.Users.ToList().Contains(userName))
context.Succeed(requirement);
return Task.FromResult(0);
}
}
Here is is the UserNamesRequirement class:
public class UserNamesRequirement : IAuthorizationRequirement
{
public UserNamesRequirement(params string[] UserNames)
{
Users = UserNames;
}
public string[] Users { get; set; }
}
UPDATE 3: SOLVED!!!!
Here are few changes that were added from update 2:
In UserNameshandler class changed var userName to get values from context.User.Identity.Name;
public class UserNamesHandler : AuthorizationHandler<UserNamesRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserNamesRequirement requirement)
{
// var userName = context.User.FindFirst(ClaimTypes.NameIdentifier).Value;
var userName = context.User.Identity.Name;
if (requirement.Users.ToList().Contains(userName))
context.Succeed(requirement);
return Task.FromResult(0);
}
}
In StartUp class fixed from services.AddSingleton<IAuthorizationHandler, UserNamesRequirement>(); to services.AddSingleton<IAuthorizationHandler,UserNamesHandler>();
Thanks to Gevory. :)
public class UserNamesHandler : AuthorizationHandler<UserNamesRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserNamesRequirement requirement)
{
var userName = context.User.Identity.Name;
if(requirement.UserNames.ToList().Contains(userName))
context.Succeed(requirement);
return Task.CompletedTask; // if it does not compile use Task.FromResult(0);
}
}
public class UserNamesRequirement : IAuthorizationRequirement
{
public UserNamesRequirement(params string[] userNames)
{
UserNames = userNames;
}
public string[] UserNames { get; set; }
}
in startup.cs add the following
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy("UserNamesPolicy",
policy => policy.Requirements.Add(new UserNamesRequirement("ggg","dsds")));
});
services.AddSingleton<IAuthorizationHandler, UserNamesHandler>()
}
Just for anyone coming to this who wants a different approach, I battled with trying to get the UserNamesHandler to register properly, to no avail. So I solved this in a different way:
static readonly string[] myUserList = { "user1", "user2", "user3" };
options.AddPolicy( "MyNameListPolicy",
policy => policy.RequireAssertion(
context => myUserList.Contains( context.User.Identity.Name ) ) );
This worked fine for me.

Resources