In a controller in an ASP.NET Core web application I want to refresh the user and claims in the cookie ticket stored on the client.
The client is authenticated and authorized, ASP.NET Core Identity stores this Information in the cookie ticket - now in some Controller actions I want to refresh the data in the cookie.
The SignInManager has a function to refresh RefreshSignInAsync, but it does not accept HttpContext.User as parameter.
[HttpPost("[action]")]
[Authorize]
public async Task<IActionResult> Validate()
{
// todo: update the Client Cookie
await _signInManager.RefreshSignInAsync(User); // wrong type
}
How do I refresh the cookie?
public static class HttpContextExtensions
{
public static async Task RefreshLoginAsync(this HttpContext context)
{
if (context.User == null)
return;
// The example uses base class, IdentityUser, yours may be called
// ApplicationUser if you have added any extra fields to the model
var userManager = context.RequestServices
.GetRequiredService<UserManager<IdentityUser>>();
var signInManager = context.RequestServices
.GetRequiredService<SignInManager<IdentityUser>>();
IdentityUser user = await userManager.GetUserAsync(context.User);
if(signInManager.IsSignedIn(context.User))
{
await signInManager.RefreshSignInAsync(user);
}
}
}
Then use it in your controller
[HttpPost("[action]")]
[Authorize]
public async Task<IActionResult> Validate()
{
await HttpContext.RefreshLoginAsync();
}
Or abstract it in an action filter
public class RefreshLoginAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
await context.HttpContext.RefreshLoginAsync();
await next();
}
}
Then use it like this in your controller
[HttpPost("[action]")]
[Authorize]
[RefreshLogin] // or simpler [Authorize, RefreshLogin]
public async Task<IActionResult> Validate()
{
// your normal controller code
}
This is also possible if the user is already logged out (their access token has expired but their refresh token is still valid).
Important note: the following only works if you have "Do you want to remember your user's devices?" set to "No" in the cognito config. If anyone knows how to get it to work with it on, please let me know.
We use the following flow (js client app connecting to .NET Core API):
User signs in using username/password (CognitoSignInManager<CognitoUser>.PasswordSignInAsync)
The client receives the token, userID, and refreshToken and stores them in localStorage.
When the original token expires (1 hour), the client gets a 401 error from the API.
The client calls another API endpoint with the userID and refreshToken which then in turn calls the code below on our user service.
If the refresh result is successful, we return the new token (AuthenticationResult.IdToken).
The client the repeats the call that originally errored in a 401 with the new token.
Here is the code we added to the User Service:
public async Task<UserLoginResult> SignInRefreshAsync(string uid, string refreshToken)
{
try
{
var result = await _cognitoIdentityProvider.InitiateAuthAsync(
new InitiateAuthRequest
{
AuthFlow = AuthFlowType.REFRESH_TOKEN_AUTH,
ClientId = _pool.ClientID,
AuthParameters = new Dictionary<string, string>
{
{ "REFRESH_TOKEN", refreshToken },
{ "SECRET_HASH", HmacSHA256(uid + _pool.ClientID, _options.UserPoolClientSecret) }
}
});
if (!result.HttpStatusCode.Successful() || string.IsNullOrEmpty(result.AuthenticationResult?.IdToken))
return new UserLoginResult(UserLoginStatus.Failed);
return new UserLoginResult(UserLoginStatus.Success, uid, null, null, result.AuthenticationResult.IdToken, null);
}
catch
{
return new UserLoginResult(UserLoginStatus.Failed);
}
}
private static string HmacSHA256(string data, string key)
{
using (var sha = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(key)))
{
var result = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(data));
return Convert.ToBase64String(result);
}
}
IAmazonCognitoIdentityProvider _cognitoIdentityProvider is resolved from DI.
AWSCognitoClientOptions _options = configuration.GetAWSCognitoClientOptions(); and IConfiguration configuration is also resolved from DI.
UserLoginResult is our class to hold the token and refresh token. Obviously, adjust accordingly.
Please note that setting SECRET_HASH may not be required based on your config is Cognito.
Related
A lot of my methods in my WebAPI looks like this. I have an authorization-filter on policy, but then i also need to validate my token and that the user provided in the token exists in DB. I am wondering if there is a way of extending the Authorize attribute so that after policy is validated, the token gets validated too. And is it also possible to return the user into the method or context if existing?
[Authorize(Policy = "Admin")]
[Route("get")]
[HttpGet]
public async Task<IActionResult> Get()
{
var (success, user) = await UserHelpers.ValidateTokenAndGetUserAsync(HttpContext, _userManager, _configuration, _logger, ControllerContext, ModelState);
if (!success || user?.CustomerId == null) return Unauthorized(HttpResponseHelper.GetErrorResponse(StatusCodes.Status401Unauthorized, _localizer[ResourceConstants.Unauthorized.UserNotFoundError].Value));
//Code
await UserHelpers.CookieToResponse(user, _configuration, _webHostEnvironment, Response, Request).ConfigureAwait(false);
return Ok();
}
First, I should address that you shouldn't customize Authorize attribute.
Using (Policy, Claim, Requirement) can help you to customize your authorization based on your business.
Then my question is that your user was in the database while issuing the token. So how can it be removed now? (The user in the JWT-Token is being validated via the signature of that)
If you want to double-check if the user is enabled, or not archived yet you can write another filter like this:
public class ApiController : Controller
{
[Authorize(Policy = "Bla")]
[CheckUserIsEnabled]
public async Task<IActionResult> Get()
{
return Ok();
}
}
public class CheckUserIsEnabledAttribute : Attribute, IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var userId = context.HttpContext.User.FindFirst(x => x.Type == System.Security.Claims.ClaimTypes.NameIdentifier);
if (userId is null)
{
context.Result = new UnauthorizedResult();
return;
}
bool exists = true;
//exists = db.CheckIfUserExistsAsync(userId);
if (!exists)
{
context.Result = new UnauthorizedResult();
return;
}
await next();
}
}
This solution works because the JWT claims will be added to current user claims on httpRequest instantiation (even if you don't put Authorize attribute on the corresponding action)
ASP.NET 5 MVC application method sets HttpContext.Response cookie.
How to read this cookie value in other method called from contoller via long call chain in same request ?
Such method does not exist in response collection interface
public interface IResponseCookies
{
void Append(string key, string value);
void Append(string key, string value, CookieOptions options);
void Delete(string key);
void Delete(string key, CookieOptions options);
}
Current request TempData values set in other methods can read. Why cookies cannot ? Should cookie settings duplicated in HttpContext.Items or is there better method ?
Background:
Shopping cart application has log method called from controllers.
It must log cartid
If user adds product first time to cart, controller creates cart id using new guid and adds cartid cookie to response.
logger method uses Request.Cookies["cartid"] to log cart it.
For first item added to cart it return null since cookie is not set to browser.
Response.Cookies["cartid"]
does not exist.
Log method can called from many places. It is difficult to pass cartid as parameter to it.
Application has log method called from controllers. It logs controller context to same database used by controllers.
Logging is performed in Error controller created using ASP.NET Core application template:
public async Task<IActionResult> Error()
{
var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
await logger.LogExceptionPage(exceptionHandlerPathFeature);
HttpContext.Response.StatusCode = 500;
return new ContentResult() {
Content ="error"
};
}
How to log Response cookie in this method et by code executed before error ?
Code which causes compile error:
public class CartController : ControllerBase
{
const string cartid = "cartid";
private readonly HttpContextAccessor ca;
public CartController(HttpContextAccessor ca)
{
this.ca = ca;
}
public IActionResult AddToCartTest(int quantity, string product)
{
ca.HttpContext.Response.Cookies.Append(cartid, Guid.NewGuid().ToString());
Log("AddToCartStarted");
return View();
}
void Log(string activity)
{
Console.WriteLine($"{activity} in cart {ca.HttpContext.Response.Cookies[cartid]}");
}
}
You cannot read the Cookies from Response. You need to read it from the Request as follow
ca.HttpContext.Request.Cookies[cartId];
cartId is the key for the value stored in Cookie.
My main application delegates authentication to an AuthService application, asking it to authenticate against an Identity Provider through OpenId Connect and return a token.
[Authorize]
[HttpGet("Login")]
public IActionResult Login()
{
return Ok(
{
Token = ?
}, OpenIdConnectDefaults.AuthenticationScheme);
}
I have an API method with the [Authorize] attribute. The user is prompted to login if not already authenticated and then the method runs. My question is can I access the token from there so I can pass it in the response?
Is there a simple way to achieve this?
You can get that information injecting a IHttpContextAccessor service in the controller and defining the service in the startup.cs class
public class HomeController
{
private readonly IHttpContextAccessor _httpContextAccessor;
public HomeController(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
//check the _httpContextAccessor.HttpContext object for information about the already logged user
}
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
The [Authorize] attribute you are putting on the method means that you are restricting access to the method to authorised users only, so putting that attribute on a Login method makes no sense, as you would have to be authenticated to call it.
What you are looking for is
[Authorize]
public class AccountController : Controller
{
[AllowAnonymous]
public ActionResult Login()
{
}
[Authorize]
public ActionResult Logout()
{
}
}
As for how to generate a token, you need to have an identity provider to take a username and password, validate it and create a token that has roles and privs.
I would suggest having a look at Identity Server 4 to generate tokens.
For info on how to connect it to OpenID see Adding User Authentication with OpenID Connect
you must get a token to access the Authorize attribute method.
reference
this
Here's what I've come up with after looking through the suggestions here:
[AllowAnonymous]
[HttpGet("Login")]
public IActionResult Login()
{
return Challenge(new AuthenticationProperties
{
RedirectUri = $"{HttpContext.Request.PathBase.Value}/GetToken"
}, OpenIdConnectDefaults.AuthenticationScheme);
}
[Authorize(AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)]
[HttpGet("GetToken")]
public IActionResult GetToken()
{
var token = _contextAccessor.HttpContext.GetTokenAsync(OpenIdConnectDefaults.AuthenticationScheme, "id_token").Result;
return Ok(new
{
Token = token
});
}
I combined two APIs. From the perspective of the client app it makes a call to a Login API and gets the token as the result. Behind the scenes the Login API redirects to a second API to get and return the token.
UPDATE:
Revisting this after a while in case anyone sees this. I don't remember exactly how, but I think there was a problem with above approach for me.
In the end I used the OnSignedIn CookieAuthenticationHandler event to intercept the token and returned it with the response in a Cookie. The Login API still returns a Challenge request.
I am using RedirectToAction method of the base API controller. The first controller needs to modify the request headers (which I seem to be able to do) but the change is not retained.
This is a simplified implementation of what I am trying to do.
[HttpGet]
public IActionResult Get(string str)
{
// The request comes with a request header with key "Authorization" and value "ABC"
HttpContext.Request.Headers.Remove("Authorization");
HttpContext.Request.Headers.Add("Authorization", "XYZ");
return RedirectToAction("B");
}
[HttpGet]
public IActionResult B()
{
var value = HttpContext.Request.Headers.First(x => x.Key == "Authorization"); // I want this to be ""XYZ" , but it remains "ABC"
return Ok();
}
Any ideas on how I can get the second Action to use the Request header I updated in the first Action.
Edit:
All our Controllers / Actions are Authenticated using Core middleware JWT Auth Policy
Only Action one allows Anonymous access.
Action one is called from an internal related solution with a "Code" (Which is used to create a JWT token) .
All other Actions require the JWT for Authentication.
The "Code" and JWT Token have independent expires and also hold some data.
The info in the "Code" is deemed more up to date. Action one can be called with both a "Code" and a JWT token.
The redirect was my way of flushing out old JWT token.
When action one is called with a valid "Code" a new JWT is created. I then want to redirect to a controller that will validate the JWT token (using middleware).
I know I can do this manually, but trying to trigger core middleware.
RedirectToAction returns a 302 response to the browser, which tells the browser to start a new request to the new URL. So your code in B() is hit after the browser initiated a whole new request. So the Request object you're looking at in B() is completely different than the one you modified in Get().
That's clearly not what you want to do. Instead of redirecting, just return B():
[HttpGet]
public IActionResult Get(string str)
{
// The request comes with a request header with key "Authorization" and value "ABC"
HttpContext.Request.Headers.Remove("Authorization");
HttpContext.Request.Headers.Add("Authorization", "XYZ");
return B();
}
But something does feel wrong about rewriting the request headers. It's kind of like rewriting history. Another way is to use HttpContext.Items to store a value:
[HttpGet]
public IActionResult Get(string str)
{
HttpContext.Items["Authorization"] = "XYZ";
return B();
}
[HttpGet]
public IActionResult B()
{
var value = HttpContext.Items["Authorization"] as string ?? HttpContext.Request.Headers.First(x => x.Key == "Authorization");
return Ok();
}
The values in HttpContext.Items exist for the life of a single request.
If you must redirect, then you can use TempData to hold data until it's read in the next request:
[HttpGet]
public IActionResult Get(string str)
{
TempData["Authorization"] = "XYZ";
return RedirectToAction("B");
}
[HttpGet]
public IActionResult B()
{
if (TempData.Contains("Authorization")) {
HttpContext.Request.Headers.Remove("Authorization");
HttpContext.Request.Headers.Add("Authorization", TempData["Authorization"]);
}
var value = HttpContext.Request.Headers.First(x => x.Key == "Authorization");
return Ok();
}
I want to add a role claim to an already authenticated windows user. My naive first approach was to add the role claim inside of a custom owin middleware which runs before WebApi. Like this:
public class IdentityMiddleware : OwinMiddleware
{
public IdentityMiddleware(OwinMiddleware next) : base(next)
{
}
public async override Task Invoke(IOwinContext context)
{
var user = context.Request.User as WindowsPrincipal;
var identity = user.Identity as ClaimsIdentity;
identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
await Next.Invoke(context);
}
}
However when providing the Authorize attribute in the controller like this.
public class TestController : ApiController
{
[Authorize(Roles = "Admin")]
public string Get()
{
return User.Identity.Name;
}
}
..I will get a 401.
I noticed that the issuer of the new claim is "Local Authority" instead of "AD Authority" could this be the reason?
Have you tried this for your authorize attribute:
[Authorize(ClaimTypes.Role, "Admin")]
This works for me:
var сlaimsIdentity = user.Identity as ClaimsIdentity;
сlaimsIdentity?.AddClaim(new Claim(сlaimsIdentity.RoleClaimType, "Admin"));
if (user.IsInRole("Admin")) ... // always true