.NET Core WebAPI permanent token authentication - authentication

I have been looking at tutorial after tutorial about securing your .NET Core WebAPI with authentication tokens and everything seems to require a username/password combo in order to get a temporary token for use to authenticate against API controllers.
The project I am working on is using Windows IOT devices running a custom UWP application I wrote that needs to connect to this API in the background in order to record data and pull down the latest device configurations.
I had planned on giving each device a unique token for authenticating that will be entered and stored during the initial device/app setup. Most third party APIs I have worked with just issue you a permanent token that you can use to access their APIs. I was wanting to do something similar.

JWT seemed overkill and overly complex for my purposes so I ended up going with a middleware solution by following this tutorial:
https://www.youtube.com/watch?v=n0llyujNGw8
I ended up creating a middleware class with the following code:
public class TokenValidationMiddleware
{
private readonly RequestDelegate _next;
public TokenValidationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
bool validToken = false;
//Require HTTPS
if (context.Request.IsHttps)
{
//Skip token authentication for test controller
if (context.Request.Path.StartsWithSegments("/api/values"))
{
validToken = true;
}
//Token header exists in the request
if (context.Request.Headers.ContainsKey("Token"))
{
//Check for a valid device by API token in my DB and set validToken to true if found
if (repository.FindDeviceByAPIKey())
{
validToken = true;
}
}
if (!validToken)
{
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
await context.Response.WriteAsync("Invalid Token");
}
else
{
await _next.Invoke(context);
}
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.HttpVersionNotSupported;
await context.Response.WriteAsync("HTTP not supported");
}
}
}
public static class TokenExtensions
{
public static IApplicationBuilder UseTokenAuth(this IApplicationBuilder builder)
{
return builder.UseMiddleware<TokenValidationMiddleware>();
}
}
Then I just added app.UseTokenAuth(); to my Startup class

You can use a standard JWT approach, creating two tokens on username/password login.
First token (Access token) is short-lived and contains privileges to access your business login endpoints. The second one (Refresh token) is permanent and allows you to acquire a new Access token, once it has expired, creating a continuous access pattern. Refresh token should only carry a refresh claim, which would allow you to access the endpoint used specifically for creating a new short lived token.
Tons of tutorials out there, like http://piotrgankiewicz.com/2017/12/07/jwt-refresh-tokens-and-net-core/

Related

How to use Auth0 for full signup-signin process

I am currently having troubles using auth0.com to set up whole authentication process for my asp.net web api project (I didn't write any view part, cause I'm using it only to learn auth/authoriz).
Their quickstart guides and docs are starting from obtaining a token for your application, I don't understand what is this token, does it grants an access to whole application or what? I wrote a default implementation with creating a user as an object, then generating a token and assigning it to user, then you pass user's email and password and log. I want to do the same using auth0.com
Is there a COMPLETE step-by-step guide on using auth0.com, with the explanation on how to create a user, how to let user log in etc.?
My default implementation:
private readonly UserManager<AppUser> _userManager;
private readonly TokenService _tokenService;
public AccountController(UserManager<AppUser> userManager, TokenService tokenService)
{
_tokenService = tokenService;
_userManager = userManager;
}
[AllowAnonymous]
[HttpPost("login")]
public async Task<ActionResult<UserDTO>> Login(LoginDTO loginDTO)
{
var user = await _userManager.FindByEmailAsync(loginDTO.Email);
if (user is null) return Unauthorized();
var result = await _userManager.CheckPasswordAsync(user, loginDTO.Password);
if (result)
{
return CreateUserObject(user);
}
return Unauthorized();
}
[AllowAnonymous]
[HttpPost("register")]
public async Task<ActionResult<UserDTO>> Register(RegisterDTO registerDTO)
{
if (await _userManager.Users.AnyAsync(x => x.UserName == registerDTO.Username))
{
ModelState.AddModelError("username", "Username taken");
return ValidationProblem();
}
if (await _userManager.Users.AnyAsync(x => x.Email == registerDTO.Email))
{
ModelState.AddModelError("email", "Email taken");
return ValidationProblem();
}
var user = new AppUser
{
DisplayName = registerDTO.DisplayName,
Email = registerDTO.Email,
UserName = registerDTO.Username
};
var result = await _userManager.CreateAsync(user, registerDTO.Password);
if (result.Succeeded)
{
return CreateUserObject(user);
}
return BadRequest(result.Errors);
}
[Authorize]
[HttpGet]
public async Task<ActionResult<UserDTO>> GetCurrentUser()
{
var user = await _userManager.FindByEmailAsync(User.FindFirstValue(ClaimTypes.Email));
return CreateUserObject(user);
}
private UserDTO CreateUserObject(AppUser user)
{
return new UserDTO
{
DisplayName = user.DisplayName,
Image = null,
Token = _tokenService.CreateToken(user),
Username = user.UserName
};
}
In general, you don't need to set up a sign-in/sign-up infrastructure with Auth0. The platform provides you with the Universal Login page where users can register or log in to your application.
The result of the authentication on the Auth0 side is one or two tokens that tell you some info about the user (ID token) and optionally what the user/application is allowed to do (access token).
To learn more about these tokens and the difference between them, read this article.
In your case, since your application is an API, you don't have to deal with user authentication directly. Your API isn't meant for users but for client applications.
To manage users, you can do it through the Auth0 Dashboard. If you want to create your own dashboard to manage users, you can do it through the Auth0 Management API. This is the library to use for .NET.
You assume that a client will call your API endpoints with the proper authorization expressed by an access token.
Take a look at this article for a basic authorization check for ASP.NET Core Web APIs, and this one for a permission-based approach. The articles also show how to test your protected API.
I hope these directions may help.

ASP.NET Core Identity: how to implement 2fa with redirect?

ASP.NET Core Identity offers built-in 2fa providers such as SMS, phone and Authenticator app. They all trigger a 2fa flow in the backend (send an SMS, start a phone call, or just ask the user for a code from their auth app). However, we're required to use an external 2fa provider whose process involves redirecting the user to a page on their URL, which will then redirect back to a page on our end which verifies the 2fa token. This is not an OAuth flow.
IUserTwoFactorTokenProvider only offers a way to return a string token. I could abuse this by returning a URL and then redirecting in the UI but that seems a misuse of the interface. Still, I'd like to have ASP.NET Identity in charge of deciding which users require 2fa, but that means it also wants to do the 2fa itself.
public class ExternalTokenProvider : IUserTwoFactorTokenProvider<ApplicationUser>
{
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<ApplicationUser> manager, ApplicationUser user)
{
return true; //all users require 2fa
}
public async Task<string> GenerateAsync(string purpose, UserManager<ApplicationUser> manager, ApplicationUser user)
{
var challenge = GenerateChallengeToken();
return $"https://2fa.example.com?user={user.Id}&token={challenge}";
}
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<ApplicationUser> manager, ApplicationUser user)
{
//validate the response token;
}
}
Looking at the implementation of AuthenticatorTokenProvider I see it simply returns string.Empty in GenerateAsync(), and then in Microsoft.AspNetCore.Identity.UI the LoginWith2fa.cshtml.cs page is called. I could do something similar but use the URL retrieved from GenerateAsync() to redirect the user.
I've also read How to register a Two-factor authentication provider which still just uses the basic functionality of sending a code via phone/e-mail. It seems the only choice you really have here is to choose the communication gateway.
Is this a proper way to do it within ASP.NET Identity? What's a better way to do this?
My implementation ended up not generating the redirect URL in the TokenProvider (because it doesn't seem to be possible to retrieve the generated token/URL from the frontend) but rather copying the implementation of Microsoft.AspNetCore.Identity.UI and just doing it in the frontend:
MfaTokenProvider.cs
public async Task<bool> CanGenerateTwoFactorTokenAsync(...)
{
return true;
}
public async Task<string> GenerateAsync(...)
{
return Task.FromResult(string.Empty);
}
public async Task<bool> ValidateAsync(...)
{
return _mfaProvider.ValidateToken(token, user.Email);
}
AccountController.cs
[HttpPost]
public async Task<IActionResult> Login(LoginForm form)
{
var result = await _signInManager.PasswordSignInAsync(form.Username, form.Password, form.RememberMe, false);
if (result.RequiresTwoFactor)
{
var authenticationUrl = _mfaProvider.GetUrl(form.Username, form.ReturnUrl);
return RedirectResult(authenticationUrl);
}
return RedirectResult(form.ReturnUrl);
}
This has the advantage of leaving RequiresTwoFactor managed by Identity. The downside is the implementation of the 2FA is split over 2 classes and can cause issues when needing to offer multiple 2FA options. This is not currently a requirement of my app, however.

ASP.NET Core 3.1 Microsoft Graph Access Token lost when application is restarted

Using ASP.NET Core 3.1 and Microsoft Graph API, when the application is restarted the Access Token is lost. The user is still logged in, but is unable to perform any Graph API calls.
I have tried the recommendations mentioned here Managing incremental consent and conditional access but was unable to refresh the token, and resume automatically.
Here is my Controller:
readonly ITokenAcquisition tokenAcquisition;
private GraphServiceClient graphServiceClient;
public HomeController(GraphServiceClient graphServiceClient, ITokenAcquisition tokenAcquisition)
{
this.graphServiceClient = graphServiceClient;
this.tokenAcquisition = tokenAcquisition;
}
[HttpGet]
[AuthorizeForScopes(Scopes = new string[] {"user.read"})]
public async Task<IActionResult> A()
{
User user;
try
{
var scopes = new string[] { "user.read" };
var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
user = await graphServiceClient.Me.Request().GetAsync();
}
catch (MicrosoftIdentityWebChallengeUserException ex)
{
// token is invalid....
// the throw causes a redirect to the User Login Page
throw ex.MsalUiRequiredException;
}
user = await graphServiceClient.Me.Request().GetAsync();
Serilog.Log.Debug("{#User}", user);
return View();
}
In the code above, when the application is restarted the access token is
lost, and re-throwing the exception causes a redirect to the Login-Page.
If I then click the Sign-in with Microsoft button, the user is already signed-in,
there is no need to enter the credentials. If I then access the controller calling the Graph API, the API calls succeed.
How can I refresh the tokens before calling the API?
Also how can I debug this? If I set a breakpoint at throw ex.MsalUiRequiredException; It is of no use, I cannot see,
where the redirect get's it's value from.
The Annotation [AuthorizeForScopes(Scopes = new string[] {"user.read"})]
Is responsible for dealing with the redirect.
For debugging, use the AuthorizeForScopes source file.
In this specific case the AuthenticationScheme was not set to a value,
and was null.
Annotate the Action method to enforce a specific Scheme.
e.g. AuthenticationScheme=OpenIdConnectDefaults.AuthenticationScheme
[HttpGet]
[AuthorizeForScopes(Scopes = new string[] {"user.read"}, AuthenticationScheme=OpenIdConnectDefaults.AuthenticationScheme)]
public async Task<IActionResult> A()
{
// snipped
}
This resulted into the desired redirect path:
https://login.microsoftonline.com/{}/oauth2/v2.0/authorize?client_id={}

ASP.NET Core enrich IIdentity with custom profile

I am using Azure AD to authorize and authenticate the users.
All users have a profile in the database.
I would like on login to always "merge" the Azure user with my database user.
This is the code I am using to setup authentication in my web api.
public static partial class ServiceCollectionExtensions
{
public static IServiceCollection AddBearerAuthentication(this IServiceCollection services,
OpenIdConnectOptions openIdConnectOptions)
{
#if DEBUG
IdentityModelEventSource.ShowPII = true;
#endif
services
.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", o =>
{
o.Authority = openIdConnectOptions.Authority;
o.TokenValidationParameters.ValidIssuer = openIdConnectOptions.ValidIssuer;
o.TokenValidationParameters.ValidAudiences = openIdConnectOptions.ValidAudiences;
});
return services;
}
}
Can someone point me in the right direction?
Right now I am loading the user in all of my controllers, not pretty at all.
Not sure what do you mean by "merge" the user. But if it's just some logic you want to run for every incoming http request, you could just add a custom middleware
app.Use(async (context, next) =>
{
var user = await context.RequestServices
.GetRequiredService<DatabaseContext>()
.Users
.Where(....)
.SingleOrDefaultAsync();
...
await next(context);
});
Alternatively, if you want to couple your code with the authentication process very much, you could use the callback from JwtBearerOptions
.AddJwtBearer("Bearer", o =>
{
...
o.Events.OnTokenValidated = async context =>
{
var user = await context.HttpContext
.RequestServices
.GetRequiredService....
...
};
}
But personally, I think both approaches are bad. Going to the DB to get the user's credentials with every request is bad for performance. Also, it kinda defies the whole point of the JWT, which was designed specifically to not do that. The token should already contain all the claims inside. If it doesn't, I would suggest reconfiguring azure AD, or switch to self-issued tokens.

OpenIdConnect access_token size and accessing claims server side

I am trying to wrap my head around several concepts here but I don't want this question to be too broad - basically what we are trying to do is use role claims as permissions to lock down our API but I am finding that the access_token is becoming too big.
We are using OpenIddict and ASP.NET Identity 3 on the server side. We have implemented the default AspNetRoleClaims table to store our claims for each role - using them as permissions.
We lock down our API endpoints using custom policy based claims authorization as shown here:
Custom Policy Based Authorization
The main issue I am finding is that our access_token containing our claims is becoming very large. We are attempting to make the ClaimType and Value to be very small in the database to make the claims footprint smaller. We have a basic CRUD type permission scheme, so for each "module" or screen in our SPA client app, there are 4 permissions. The more modules we add to our application, the more the claims are growing in the access_token and our Authorization Bearer header is becoming very large. I am worried about this becoming not very scalable as the app grows.
So the claims are embedded in the access_token and when I hit my endpoint that is locked down with a custom Policy like this...
[Authorize(Policy="MyModuleCanRead")]
[HttpGet]
public IEnumerable<MyViewModel> Get()
I can then access my ASP.NET Identity User and User.Claims in the AuthorizationHandler.
Sorry in advance if this is an obvious question - but I am wondering - in order to get the Custom Policy Based Authorization to work - does it absolutely require the claims to be in either the id_token or the access_token in order to call the handler?
If I remove the claims from the access_token, then my AuthorizationHandler code does not get hit and I cannot access my endpoint that is locked down with my custom Policy.
I am wondering if it is possible to use a custom claims policy but have the actual code that checks for the Claims inside the Authorization handler, so that the claims are not passed with each HTTP request, but are fetched server side from the Authorization cookie or from the database.
* UPDATE *
Pintpoint's answer using Authorization handlers along with the comment on how to remove additional role claims from the cookie achieved just what I was looking for.
In case this helps anyone else - here is the code to override the UserClaimsPrincipalFactory and prevent the role claims from being written to the cookie. (I had many role claims as permissions and the cookie(s) and request headers were becoming too large)
public class AppClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
public AppClaimsPrincipalFactory(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor)
{
}
public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var userId = await UserManager.GetUserIdAsync(user);
var userName = await UserManager.GetUserNameAsync(user);
var id = new ClaimsIdentity(Options.Cookies.ApplicationCookieAuthenticationScheme,
Options.ClaimsIdentity.UserNameClaimType,
Options.ClaimsIdentity.RoleClaimType);
id.AddClaim(new Claim(Options.ClaimsIdentity.UserIdClaimType, userId));
id.AddClaim(new Claim(Options.ClaimsIdentity.UserNameClaimType, userName));
if (UserManager.SupportsUserSecurityStamp)
{
id.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType,
await UserManager.GetSecurityStampAsync(user)));
}
// code removed that adds the role claims
if (UserManager.SupportsUserClaim)
{
id.AddClaims(await UserManager.GetClaimsAsync(user));
}
return new ClaimsPrincipal(id);
}
}
I am wondering if it is possible to use a custom claims policy but have the actual code that checks for the Claims inside the Authorization handler, so that the claims are not passed with each HTTP request, but are fetched server side from the Authorization cookie or from the database.
It's definitely possible. Here's how you could do that:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("Has-Edit-User-Profiles-Permission", builder =>
{
builder.RequirePermission("Edit-User-Profiles");
});
});
}
}
public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
public PermissionAuthorizationRequirement(string permission)
{
if (string.IsNullOrEmpty(permission))
{
throw new ArgumentException("The permission cannot be null or empty.", nameof(permission));
}
Permission = permission;
}
public string Permission { get; set; }
}
public class PermissionAuthorizationHandler :
AuthorizationHandler<PermissionAuthorizationRequirement>
{
private readonly UserManager<ApplicationUser> _userManager;
public PermissionAuthorizationHandler(UserManager<ApplicationUser> userManager)
{
if (userManager == null)
{
throw new ArgumentNullException(nameof(userManager));
}
_userManager = userManager;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionAuthorizationRequirement requirement)
{
if (context.User == null)
{
return;
}
var user = await _userManager.GetUserAsync(context.User);
if (user == null)
{
return;
}
// Use whatever API you need to ensure the user has the requested permission.
if (await _userManager.IsInRoleAsync(user, requirement.Permission))
{
context.Succeed(requirement);
}
}
}
public static class PermissionAuthorizationExtensions
{
public static AuthorizationPolicyBuilder RequirePermission(
this AuthorizationPolicyBuilder builder, string permission)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (string.IsNullOrEmpty(permission))
{
throw new ArgumentException("The permission cannot be null or empty.", nameof(permission));
}
return builder.AddRequirements(new PermissionAuthorizationRequirement(permission));
}
}