In my MVC 5 web app I have this (in AccountController.cs):
// Used for XSRF protection when adding external sign ins
private const string XsrfKey = "XsrfId";
and
public string SocialAccountProvider { get; set; }
public string RedirectUri { get; set; }
public string UserId { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
if (UserId != null)
{
properties.Dictionary[XsrfKey] = UserId;
}
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, SocialAccountProvider);
}
How exactly is it being used for protection?
Should I set the value of XsrfKey to something more random?
Take a look at ManageController methods LinkLogin and LinkLoginCallback:
//
// POST: /Manage/LinkLogin
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LinkLogin(string provider)
{
// Request a redirect to the external login provider to link a login for the current user
return new AccountController.ChallengeResult(provider, Url.Action("LinkLoginCallback", "Manage"), User.Identity.GetUserId());
}
//
// GET: /Manage/LinkLoginCallback
public async Task<ActionResult> LinkLoginCallback()
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, User.Identity.GetUserId());
if (loginInfo == null)
{
return RedirectToAction("ManageLogins", new { Message = ManageMessageId.Error });
}
var result = await UserManager.AddLoginAsync(User.Identity.GetUserId(), loginInfo.Login);
return result.Succeeded ? RedirectToAction("ManageLogins") : RedirectToAction("ManageLogins", new { Message = ManageMessageId.Error });
}
These are the methods that handle linking of external accounts (i.e. Google, Facebook, etc.). The flow goes like this:
User clicks "Link Account" button, which calls a POST to LinkLogin method.
LinkLogin returns ChallengeResult object, with callback url set to LinkLoginCallback method.
ChallengeResult.ExecuteResult is called by MVC framework, calls IAuthenticationManager.Challenge, which causes a redirect to the specific external login provider (let's say: google).
User authenticates with google, then google redirects to callback url.
The callback is handled with LinkLoginCallback. Here, we want to prevent XSRF and verify that the call was initiated by a user, from a page served by our server (and not by some malicious site).
Normally, if it was a simple GET-POST sequence, you would add a hidden <input> field with an anti-forgery token and compare it with a corresponding cookie value (that's how Asp.Net Anti-Forgery Tokens work).
Here, the request comes from external auth provider (google in our example). So we need to give the anti-forgery token to google and google should include it in the callback request. That's exactly what state parameter in OAuth2 was designed for.
Back to our XsrfKey: everything you put in AuthenticationProperties.Dictionary will be serialized and included in the state parameter of OAuth2 request - and consequentially, OAuth2 callback. Now, GetExternalLoginInfoAsync(this IAuthenticationManager manager, string xsrfKey, string expectedValue) will look for the XsrfKey in the received state Dictionary and compare it to the expectedValue. It will return an ExternalLoginInfo only if the values are equal.
So, answering your original question: you can set XsrfKey to anything you want, as long as the same key is used when setting and reading it. It doesn't make much sense to set it to anything random - the state parameter is encrypted, so no one expect you will be able to read it anyway.
Just leave it as is:
As the name of the member states it is a key:
private const string XsrfKey = "XsrfId";
It is defined in this manner to avoid "magic numbers" and then is used a little down in the scaffold code:
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
if (UserId != null)
{
properties.Dictionary[XsrfKey] = UserId;
}
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
}
The value of the dictionary item is then set to the UserId property in the above code by using the XsrfKey member as the key.
IOW the code is already setting the XSRF dictionary item to the value of the user ID in the snippet. If you change the XsrfKey members value to anything else you will cause problems down the line, since the expected key "XsrfId" will have no value set.
If by changing it to something more random you are implying to change the value and not they key of the dictionary, or in other words, not set it to the user id then please see the following for an explanation of the anti forgery token inner workings.
http://www.asp.net/mvc/overview/security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages
Related
I am developing an intranet asp.net core web api application. The requirements for authentications are:
REQ1 - when user which is trying to access the website is not in Active Directory's special group (let's name it "commonUsers") it is simply not authorized
REQ2 - when user which is trying to access the website is in Active Directory's group "commonUsers" is is authorized and a web resource is returned
REQ3 - when user which is trying to access the website is in Active Directory's group "superUser", it need to be prompted for his domain password once again (because it tries to access some very restricted resources)
Now, what I have so far:
My service is hosted using http.sys server in order to support windows authentication.
I am using claims transformer middlewere in order to check the user's Active Directory group, let's say something like this:
public class ClaimsTransformer : IClaimsTransformation {
private readonly IAuthorizationService _authorizationService;
public ClaimsTransformer(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
_authorizationService.Authorize(principal as IHmiClaimsPrincipal);
return Task.FromResult(principal);
}}
I have specified a special policies also in my service configuration, for instance something like that:
services.AddAuthorization(options =>
{
options.AddPolicy("TestPolicy", policy =>
policy.RequireClaim(ClaimTypes.Role, "TestUser"));
options.AddPolicy("TestPolicy2", policy =>
policy.RequireClaim(ClaimTypes.Role, "SuperUser"));
});
I am using [Authorize] attribute with specific policy in order to restrict access to specific resources based on policies
Now the question is, how should I satisfy REQ3?
I think I would try to use MVC Filters : https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#authorization-filters
Filters run after all Middleware, but before the Action. This will allow you to control the redirect to credentials page just for specific actions or controllers. Whilst normally this is not the recommended method for authorization, I think it fits your requirements for a hybrid secondary authentication.
public class SuperUserFilter : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.Request.Cookies.TryGetValue("SuperUserCookie", out string cookieVal))
{
if (!IsValidCookie(cookieVal))
context.Result = LoginPage(context);
}
else
{
context.Result = LoginPage(context);
}
}
private bool IsValidCookie(string cookieVal)
{
//validate cookie value somehow
// crytpographic hash, store value in session, whatever
return true;
}
private ActionResult LoginPage(AuthorizationFilterContext context)
{
return new RedirectToActionResult("SuperUser", "Login",
new {redirectUrl = context.HttpContext.Request.GetEncodedUrl()});
}
}
Then you create a Login Controller
public class LoginController : Controller
{
[HttpGet]
public IActionResult SuperUser(string redirectUrl)
{
// return a page to enter credentials
// Include redirectUrl as field
}
[HttpPost]
public IActionResult SuperUser(LoginData loginData)
{
// Validate User & Password
Response.Cookies.Append("SuperUserCookie", "SomeValue");
return Redirect(loginData.RedirectUrl);
}
}
Then you can decorate specific actions (or controllers) as required:
public class MyController : Controller
{
[HttpGet]
[SuperUserFilter]
public IActionResult MySensitiveAction()
{
// Do something sensitive
}
}
I'm guessing you are try to implement two step authentication for some of your resource.
To do that you must use multiple authentication scheme and Authorize policies,
but it's difficult because windows authentication is not controllable. we need to use some trick to know this is your second login.
authentication
The Default Authenticaiton Scheme : Windows, it's the basic scheme for authenticate a windows user.
Second Cookies base Authentication scheme : SuperUserTwoStep. we need this to goto our custom login logic.
Authorize
the Authorize policies for specified scheme.
a login page for login to SuperUserTwoStep scheme.
//startup
services.AddAuthentication(HttpSysDefaults.AuthenticationScheme)
.AddCookie("SuperUserTwoStep",op=>op.LoginPath = "/account/superuser2steplogin");
services.AddAuthorization(op =>
{
op.AddPolicy("SuperUser", b => b.AddAuthenticationSchemes("SuperUserTwoStep")
.RequireAuthenticatedUser()
.RequireClaim(ClaimTypes.Role, "SuperUser"));
});
// login
public static IDictionary<string, string> States { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
[Route("/account/superuser2steplogin")]
public async Task<IActionResult> LoginTwoStepConfirm(string returnUrl, [FromServices]IAuthorizationService authorizationService,
[FromServices]IAuthorizationPolicyProvider policyProvider)
{
var winresult = await HttpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme);
if (winresult.Succeeded)
{
if (States.TryGetValue(winresult.Principal.Identity.Name, out _))
{
States.Remove(winresult.Principal.Identity.Name);
var principal = new System.Security.Claims.ClaimsPrincipal(new System.Security.Claims.ClaimsIdentity(winresult.Principal.Claims,"twostepcookie"));
await HttpContext.SignInAsync("SuperUserTwoStep", principal);
return Redirect(returnUrl);
}
else
{
States[winresult.Principal.Identity.Name] = "1";
return Challenge(IISDefaults.AuthenticationScheme);
}
}
else
{
return Challenge(IISDefaults.AuthenticationScheme);
}
}
[Authorize("SuperUser")]
public IActionResult YourSecurePage()
{
return Content("hello world");
}
the most difficult thing is to track that this is the second time to login, I try to use cookie , but it doen't work, so I crate a static IDitionary<string,string> to track ,maybe use distributed cache is better
I think in my opinion you should consider using: Policy-based authorization with Requirements, basically you have different authorization requirements that you want to treat them on and AND basis
REQ1 and REQ2 and REQ3
Here you have the link to the documentation: Requirements
But you need to understand that identity != permissions, the guys that introduce this concept of policies to Microsoft created a project named: PolicyServer and it is opensource: PolicyServer Git and they created a pattern there of how you should use your policies. Basically, you have external and internal users that are authenticated against your AD, all internal users should have permissions assigned to a role. And you only decorate your controller action with the permission rule you created for that policy
[Authorize("PerformSurgery")]
public async Task<IActionResult> PerformSurgery()
{
// omitted
}
To understand the code and how they evaluate a policy, I think you should see the video they have online on the website: Policy Server
Hope this helps
I'm developing a custom middleware for authenticating clients that invokes an API.
I use an attribute to define if an Action requires authentication, but I can't figure out how to get a reference to the requested Controller and Action inside the Invoke method.
Below is my code so far
AuthenticateClient.cs:
public class AuthenticateClient
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly GenericUnitOfWork _worker;
public AuthenticateClient(RequestDelegate next, ApiDbContext db, IHttpContextAccessor httpContext, IHostingEnvironment env, ILoggerFactory loggerFactory, IOptions<Utility.LCLog.Settings> settings)
{
_next = next;
_logger = loggerFactory.CreateLogger(settings.Value.ApplicationName);
_worker = new GenericUnitOfWork(new AppHelper(httpContext, db, env));
}
public async Task Invoke(HttpContext context)
{
if (!context.Request.Headers.Keys.Contains("ClientAuth"))
{
_logger.LogWarning("ClientAuth missing in request", new string[] { "Host: " + context.Request.Host, "IP: " + context.Request.HttpContext.Connection.RemoteIpAddress });
context.Response.StatusCode = 400;
await context.Response.WriteAsync("ClientAuth missing from request header values");
return;
}
else
{
string[] tmp = context.Request.Headers["ClientAuth"].ToString().Split("/");
if (tmp.Length != 2)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("The format of the ClientAuth value is wrong");
return;
}
else
{
Client client;
string key, pass;
key = tmp[0];
pass = tmp[1];
client = await _worker.GetRepo<Client>().SingleOrDefault(clnt => clnt.Active && clnt.Key.Equals(key) && clnt.Password.Equals(pass));
if (client == null)
{
_logger.LogWarning("Client authentication failed", new string[] { "Key: " + key, "Password: " + pass, "Host: " + context.Request.Host, "IP: " + context.Request.HttpContext.Connection.RemoteIpAddress });
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Authentication failed");
return;
}
}
}
await _next.Invoke(context);
}
}
ClientAuthenticationAttribute.cs:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ClientAuthenticationAttribute : Attribute
{
private readonly bool _authRequired;
public ClientAuthenticationAttribute(bool authRequired = true)
{
_authRequired = authRequired;
}
public bool AuthRequired { get { return _authRequired; } }
}
I'd recommend you to split your logic for authentication and authorization and keep them in different places.
To recap from here:
Authentication is the process of verifying who you are.
Authorization is the process of verifying that, given that we know who you are, you have access to the specific resource.
What you're currently trying to do, is to both authenticate and authorize your user in the middleware component. Although you could probably get it to work by moving all such logic into filters which you register with the api framework (be it ASP.NET Core MVC, Web API 2 or something else), that would mean that none of your other middleware components have access to the user data (which, I'm guessing, is one of the reasons you chose to implement it in a middleware in the first place).
Given your new knowledge of the separation of authentication and authorization, a possible solution would be to do the following:
Middleware for authentication only
In your middleware, concern yourself only with authentication, and leave authorization up to components later in the pipeline. In practice, this means that your middleware should do the following:
Look for user tokens, cookies or whatever you use for the users to authenticate their request
If not present, treat the request as anonymous, and call the next pipeline component without attaching a user to the request context.
If a valid token is present, resolve the user data from it (e.g. parse the user's claims from a JWT, look up roles in a database, etc...) and store it on the request context. I've found it useful both to create an IPrincipal and set context.Request.User to it, as well as adding information to the context dictionary directly.
With the user registered in the request context, call the next pipeline component.
Authorization assuming an authenticated user
You can now re-write your authorization logic to assume that there's already an authenticated user registered on the request context.
In an ASP.NET Web API 2 application, you'd implement a custom filter attribute inheriting from AuthorizationFilterAttribute, to make sure it runs first of the filters. In my current application, for example, we have the following attribute to authorize that a user has a specific claim. Note that it doesn't do any work to figure out who the user is; if a user is not attached to the context, the response is simply Unauthorized. You could be more sophisticated here, and treat anonymous requests differently from authenticated requests for users who lack access, and, for example, redirect anonymous requests to the login form, while redirecting users lacking access to an error page stating as much.
[AttributeUsage(validOn: AttributeTargets.Method)]
public class AuthorizeClaimsFilterAttribute : AuthorizationFilterAttribute
{
public AuthorizeClaimsFilterAttribute(string claimType, string claimValue)
{
ClaimType = claimType;
ClaimValue = claimValue;
}
public string ClaimType { get; }
public string ClaimValue { get; }
public override void OnAuthorization(HttpActionContext actionContext)
{
if (!(actionContext.RequestContext.Principal is ClaimsPrincipal principal)
|| !principal.HasClaim(x => x.Type == ClaimType && x.Value == ClaimValue))
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
}
}
}
To use it, we just decorate the action method with it:
[AuthorizeClaimsFilter("urn:ourapp:claims:admin", true)]
public IHttpActionResults OnlyAdminsCanAccess() { /* ... */ }
I have an action method that uses my authentication filter:
public class TutorAuthenticationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var req = filterContext.HttpContext.Request;
var auth = req.Headers["Authorization"];
if (!string.IsNullOrEmpty(auth))
{
var cred = System.Text.Encoding.ASCII.GetString(Convert.FromBase64String(auth.Substring(6))).Split(':');
var user = new { Name = cred[0], Password = cred[1] };
if (userService.AuthorizeTutor(user.Name, user.Password))
{
return;
}
}
filterContext.HttpContext.Response.AddHeader("WWW-Authenticate", $"Basic realm= {BasicRealm}");
filterContext.Result = new HttpUnauthorizedResult();
}
}
I would like to then display on main page something for user that have been authenticated this way, but this does not work in my View :(
#if (Request.IsAuthenticated)
{
<h1>Hello</h1>
}
I know it does not work because I don't use Identity, but is there any way that I can do this?
Thank you for answers :)
I suppose, that sending login and password in header is not secure. Better solution is one time when user is verified. And after checking, you can check all request.
For example, if you use FormsAuthentication and authCookie it's very simple:
Set auth mentod in web.config: <authentication mode="Forms" />
When login and password is valid, use FormsAuthentication.SetAuthCookie(userName, createPersistentCookie = true); This step is performed only once, when user login to application.
Then you can use property this.Request.IsAuthenticated in view or HttpContext.Current.Request.IsAuthenticated in controller (or filter).
And it works attribute [Authorize] on conntrolers or actions (public methods in conntrollers). When request is not authenticated, request is redirected to default (or set in web.config) login page.
Create a new extension method for the request object say (IsUserAuthenticated()) & in that method check if the user is valid.
Once this is done, you can use this new extension method the same way you are using Request.IsAuthenticated Property.
Below is the sample code, which you will need to tweak as per your needs. (specifically for
userservice
initialization)
public class RequestValidator
{
public bool IsValid(HttpRequest request)
{
bool isValid = false;
//TODO: Intitialize your userService here, may be using DI or a concrete object creation depending on your implementation
var auth = request.Headers["Authorization"];
if (!string.IsNullOrEmpty(auth))
{
var cred = System.Text.Encoding.ASCII.GetString(Convert.FromBase64String(auth.Substring(6))).Split(':');
var user = new { Name = cred[0], Password = cred[1] };
isValid = userService.AuthorizeTutor(user.Name, user.Password))
}
return isValid;
}
}
Your attribute will change like this
public class TutorAuthenticationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var req = filterContext.HttpContext.Request;
RequestValidator validator = new RequestValidator();
if(validator.IsValid(request))
{
return;
}
filterContext.HttpContext.Response.AddHeader("WWW-Authenticate", $"Basic realm= {BasicRealm}");
filterContext.Result = new HttpUnauthorizedResult();
}
}
And the extension method to be used on view will be
public static class Extensions
{
public static bool IsUserAuthenticated(this HttpRequest request)
{
RequestValidator validator = new RequestValidator();
return validator.IsValid(request);
}
}
Use it like this
#if(Request.IsUserAuthenticated())
{
<p>Hello</p>
}
If you want to pass the boolean value indicating if the user is authenticated, maybe it makes sense to just use the model object and pass it to the view.
Or maybe you should review your Form Authentication to make Request.IsAuthenticated working properly. This thread will help to start digging.
Another option would be to consider using the IAuthorizationFilter instead of the custom action filter. This thread will be a starting point.
Hope that helps!
To meet your purpose, you would need to set HttpContext.User to some valid IPrincipal.
So, if according to your criteria, the user is valid you just need to create a GenericPrinicpal and set HttpContext.User with the instance you have just created.
Something like this:
var genericIdentity=new GenericIdentity(user.Name, "CustomAuthType");
var genericPrincipal=new GenericPrincipal(genericIdentity, null);
HttpContext.User = genericPrincipal;
With GenericIdentity, the value of IsAuthenticated is dependent on the Name property, so as soon as the GenericIdentity has a Name, it is considered to be authenticated.
In this example, I'm setting the HttpContext.User and not the Thread.CurrentPrincipal so that you can get the IsAuthenticated from the Request.IsAuthenticated property.
Some extra and related information:
GenericIdentity Class
Principal and Identity Objects
Create GenericPrincipal and GenericIdentity Objects
Replacing a Principal Object
in your startup.cs file add this
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Login"),
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromMinutes(40)
});
I am using SignalR to relay messages from a WebAPI server back-end to a JavaScript web page. These messages are only relayed to certain users so I need to map the SignalR ConnectionId with the custom id of the user of the webpage.
Currently the WebAPI uses FormsAuthentication and the custom id I need is in the cookie.
Initially I inherited the IUserIdProvider to pull the value off of the cookie:
public class CustomIdProvider : IUserIdProvider
{
public string GetUserId(IRequest request)
{
Cookie formsAuthCookie = request.Cookies[FormsAuthentication.FormsCookieName];
try
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(formsAuthCookie.Value);
var obj = JsonConvert.DeserializeObject(ticket.Name) as JObject;
return (string)obj.GetValue("UserId");
}
catch (Exception)
{
return null;
}
}
}
which worked as far as getting the custom id correctly. But that value was never set on the identity as far as I could tell. I also was unable to edit any of the Identity values due to Context.User.Identity.Name all being readonly.
Edit: Trying the CustomIdProvider again, I correctly get the value out of the cookie but on returning from the GetUserId method, OnConnected is never called.
My next approach was based off of Shaun Xu's blogpost
Set Context User Principal For Customized Authentication In SignalR.
Here is my implementation:
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
private const string CUSTOM_IDENTITY_KEY = "server.User";
public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
{
string customId;
// This gets the custom id from the cookie.
TryGetCustomId(request, out customId);
// The CustomIdentity class just sets the customId to the name.
request.Environment.Add(CUSTOM_IDENTITY_KEY, new ClaimsPrincipal(new CustomIdentity(customId, true)));
return true;
}
public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
{
string connectionId = hubIncomingInvokerContext.Hub.Context.ConnectionId;
IDictionary<string, object> environment = hubIncomingInvokerContext.Hub.Context.Request.Environment;
object obj;
environment.TryGetValue(CUSTOM_IDENTITY_KEY, out obj);
var principal = obj as ClaimsPrincipal;
if (principal?.Identity == null || !principal.Identity.IsAuthenticated)
{
return false;
}
hubIncomingInvokerContext.Hub.Context = new HubCallerContext(new ServerRequest(environment), connectionId);
return true;
}
and this actually works to set the Context.User.Identity correctly when both methods are invoked.
However, my problem is that when the user first connects and OnConnected is called, it does not call the AuthorizeHubMethodInvocation and therefore the Context.User.Identity is not available in OnConnected.
I want to be able to access the correct Identity containing my custom id at all stages of the SignalR hub: OnConnected, OnDisconnected, and invoked methods.
Does anyone have a good solution for this?
I have some comments:
1) You should not try to establish your custom identity in authorization stage. It should be the concern of the authentication stage.
2) Implementing custom IUserIdProvider and establishing custom identity id of your Context.User.Identity are separate concerns. The custom IUserIdProvider is just to map any of the properties of your Context.User.Identity as the identifier of user in signalR.
So, to fix your problem. Try establishing your custom identity id at authentication stage. There are many ways to do it depending on how you setup your application. For example:
1) Establishing your custom identity id in Application_PostAuthenticateRequest:
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
//Provide custom identity id to your HttpContext.Current.User
//In your case, you may extract that information from your authentication ticket.
}
You could look at this post if you need detailed information: ASP.NET MVC - Set custom IIdentity or IPrincipal
2) Using claims identity, you can return the custom id as a claim, everytime when the browser sends a request to your server, your claims identity is re-established. In the example below, I use owin cookie authentication.
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, user.Id.ToString()));
var id = new ClaimsIdentity(claims, "Cookies");
var ctx = Request.GetOwinContext();
var authenticationManager = ctx.Authentication;
authenticationManager.SignIn(id);
Then in your IUserIdProvider implementation, you can extract the corresponding information in your identity (Name property, claim,...) to use as user identifier in your signalR application.
I have a WebApi controller that initially authenticates as a specific WebApi user. Subsequent accesses to the web api will pass a user that operations should be performed as, without having to actually authenticate as that user.
I have some services/managers that perform functions as those proper users as part of an MVC project. I now want to use those services and managers with the WebApi project, but I don't want to have to pass the user around.
I'm hoping I can temporarily change the identity of the Web Api call after the user passed in the Web Api call has been validated, but I want to make sure that when the call is complete, the cookie returned is for the validation of the WebApi user, not the end user that is represented as a part of the call.
My question is, what can I do to temporarily change the identity to the validated user in the call, and then change back to the web api identity?
Loosely using code from the links in the post, I created a IDisposable object that would temporarily change the identity.
Usage is:
try
{
using(new Impersonate(userManager, userName))
{
/* do your stuff as userName */
}
}
catch (ImpersonateException) {}
The Impersonate class is as follows:
public class Impersonate : IDisposable
{
private UserManager<ApplicationUser> userManager;
public Impersonate(UserManager<ApplicationUser> userManager, string userName)
{
this.userManager = userManager;
if (ValidateUser(userName))
{
this.ImpersonateUser(userName);
}
else
{
throw new ImpersonateException("Current user does not have permissions to impersonate user");
}
}
private bool ValidateUser(string userName)
{
/* validate that the current user can impersonate userName */
}
public void Dispose()
{
this.RevertImpersonation();
}
private void ImpersonateUser(string userName)
{
var context = HttpContext.Current;
var originalUsername = context.User.Identity.Name;
var impersonatedUser = this.userManager.FindByName(userName);
var impersonatedIdentity = impersonatedUser.GenerateUserIdentity(this.userManager, "Impersonation");
impersonatedIdentity.AddClaim(new Claim("UserImpersonation", "true"));
impersonatedIdentity.AddClaim(new Claim("OriginalUsername", originalUsername));
var impersonatedPrincipal = new ClaimsPrincipal(impersonatedIdentity);
context.User = impersonatedPrincipal;
Thread.CurrentPrincipal = impersonatedPrincipal;
}
private void RevertImpersonation()
{
var context = HttpContext.Current;
if (!ClaimsPrincipal.Current.IsImpersonating())
{
throw new ImpersonationException("Unable to remove impersonation because there is no impersonation");
}
var originalUsername = ClaimsPrincipal.Current.GetOriginalUsername();
var originalUser = this.userManager.FindByName(originalUsername);
var originalIdentity = originalUser.GenerateUserIdentity(this.userManager);
var originalPrincipal = new ClaimsPrincipal(originalIdentity);
context.User = originalPrincipal;
Thread.CurrentPrincipal = originalPrincipal;
}
}
This differs the linked code in that it only sets the identity temporarily, so doing having to SignIn/SignOut is not required.
Also, since the bulk of the work is done in the constructor, I had to remove the Async aspects that the linked code uses. There might be a way around it, but I'm not experienced enough with Async, or patient enough to bother.