Sadly documentation on the implementation of a custom AuthorizeInteractionResponseGenerator in IdentityServer4 is sorely lacking.
I'm trying to implement my own AuthorizeInteractionResponseGenerator because I need a further step of user interaction (after authentication). My scenario is that a single identity (email) can be associated with multiple tenants. So after logon, I need the user to be presented with a list of associated tenants, so that they can choose one.
I have evaluated the source code, and have come up with the the following custom AuthorizeInteractionResponseGenerator:
public class AccountChooserResponseGenerator : AuthorizeInteractionResponseGenerator
{
public AccountChooserResponseGenerator(ISystemClock clock,
ILogger<AuthorizeInteractionResponseGenerator> logger,
IConsentService consent, IProfileService profile)
: base(clock, logger, consent, profile)
{
}
public override async Task<InteractionResponse> ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
{
var response = await base.ProcessInteractionAsync(request, consent);
if (response.IsConsent || response.IsLogin || response.IsError)
return response;
return new InteractionResponse
{
RedirectUrl = "/Organization"
};
}
}
It inherits from the base AuthorizeInteractionResponseGenerator built into IdentityServer4, so that the standard Logon and Consent pages can show. This happens, and then the user is correctly redirected to the /Organization url to select an organization (tenant).
But what then? With the lack of documentation and examples, I'm really struggling to figure out the following two questions:
1) How do I now, having selected a Tenant, indicate to my custom AccountChooserResponseGenerator that my interaction is complete, and that the user can now be redirected back to the Client?
Edit:
Answer to 1: To indicate that the interaction is complete, you I have to return an empty new InteractionResponse(). In my case, a check for the existence of the TenantId claim sufficed, as follows:
if (!request.Subject.HasClaim(c=> c.Type == "TenantId" && c.Value != "0"))
return new InteractionResponse
{
RedirectUrl = "/Organization"
};
return new InteractionResponse();
2) And how can I get information about the selected Tenant to be added to the identity token that IdentityServer4 passes back to the Client?
Edit: Answer to 2: In the Controller Action method that gets executed after selecting a Tenant, I called :
await HttpContext.SignInAsync(User.Claims.Single(r=> r.Type == "sub").Value,
new System.Security.Claims.Claim("TenantId", tenant.Id.ToString()));
return Redirect(ReturnUrl);
...which is an IdentityServer4-provided Extension to HttpContext.
Related
I have an existing ASP.NET Core MVC application that I want to add a new API method in a controller. I want to touch the authentication/authorization code as little as possible, so I don't want to write a new authentication middleware for this one API.
The method is supposed to return some type of data if the user is anonoymous/unknown. And it should return more specific and potentially personal information if the user is signed in and has a valid user.
Is it safe to skip the [Authorize] attribute, and check only check the HttpContext.User (https://learn.microsoft.com/en-us/dotnet/api/system.web.httpcontext.user?view=netframework-4.8) property for the username?
Can I assume that the signature has been verified by the OpenID Connect middleware?
// No Authorize attribute
public IActionResult MyApi()
{
string userName = System.Security.Claims.Claim idClaim = this.HttpContext.User?.Claims?.FirstOrDefault(a => a.Type == "sub")?.Value;
// Can I assume that the User object only is non-null if the OAuth signature is actually verified?
// Or is fake-cookies possible?
}
Authorization does not do anything in regards to authentification, it's only a mechanism to check whether the authenticated user has access to the requested resource. It's like a layer on top of authentication.
The presence of HttpContext.User does not necessarily indicate an authenticated user, but HttpContext.User.Identity.IsAuthenticated does. For ClaimsIdentity it basically just checks whether the field AuthenticationType is not empty.
Therefore, it's safe to assume the user is authenticated if it has AuthenticationType field set. It's also safe to assume that if the field Name is not empty as unauthenticated users can't have it set.
I use a custom extensions method to read the username from the current identity:
public static bool TryGetUserName(this IIdentity identity, out string username)
{
try
{
username = Microsoft.AspNet.Identity.IdentityExtensions.GetUserName(identity);
return true;
}
catch (ArgumentNullException ane)
{
username = null;
return false;
}
}
And I use this extension in my API method by calling:
bool isAuth = this.User?.Identity?.TryGetUserName(out username) ?? false;
I have front app on angular 5 and backend api on c# using identity server.
The problem is that when I click logout button, the token is removed and i am redirected to logout page.
But when I try to refresh main page, I am redirected to microsoftonline.com
authenticated automatically and redirected back to main page
I am missing providing username and password here, and this occurs in chrome incognito.
What I noticed is that if I remove manually the cookie from microsoftonline.com
and repeat the process, this time I will be asked for username and password.
So first I tried to clean all cookies this way but it din't help
foreach (var key in HttpContext.Request.Cookies.Keys)
{
HttpContext.Response.Cookies.Append(key, "", new CookieOptions() { Expires = DateTime.Now.AddDays(-1) });
}
bellow is my accountcontroller logout method and cookie screen
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutViewModel model)
{
var idp = User?.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
var subjectId = HttpContext.User.Identity.GetSubjectId();
if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider)
{
if (model.LogoutId == null)
{
model.LogoutId = await interaction.CreateLogoutContextAsync();
}
try
{
await signInManager.SignOutAsync();
}
catch (NotSupportedException)
{
}
}
// set this so UI rendering sees an anonymous user
HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await interaction.GetLogoutContextAsync(model.LogoutId);
var vm = new LoggedOutViewModel
{
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = logout?.ClientId,
SignOutIframeUrl = logout?.SignOutIFrameUrl
};
await persistedGrantService.RemoveAllGrantsAsync(subjectId, "angular2client");
return View("LoggedOut", vm);
}
If I understand correctly you are federating to Microsoft from your IdentityServer4 service? If so when you sign out of your identity service you should also give the user the option to sign out of the external provider (if it supports the relevant feature - it'd need to define an end_session_endpoint in the discovery document).
This functionality is supported by the standard OIDC middleware so you should be able to initiate signout by calling SignoutAsync() and passing the name of the scheme for the MS federated sign in.
Another option is to always send prompt=login in your external sign in requests and then check the auth_time claim you get back. That way to you force interactive sign in always and also verify when it happened.
Try cleaning the cookies from the HttpContext itself, using the extension method, provided by Identity Server, like here.
Or try this:
await HttpContext.SignOutAsync(IdentityServerConstants.DefaultCookieAuthenticationScheme);
in your Logout controller method.
3rd option (what I have in one of my test MVC clients is):
public ActionResult Logout()
{
Request.GetOwinContext().Authentication.SignOut();
return Redirect("/");
}
public void SignoutCleanup(string sid)
{
var cp = (ClaimsPrincipal)User;
var sidClaim = cp.FindFirst("sid");
if (sidClaim != null && sidClaim.Value == sid)
{
Request.GetOwinContext().Authentication.SignOut("Cookies");
}
}
Where the Logout method is called on the button click, and the SignoutCleanup is the one that is passed to Identity Server, when registering the client as a Client.BackChannelLogoutUri (or Client.FrontChannelLogoutUri, or both, depending on your scenario).
PS: Now, in general I think that your approach is not right, but I don't know your full case, so I'm not judging you - just giving and advice.
For front-end clients (Angular, Vue, vanilla JS etc..) it is recommended to use the client-side oidc-client-js library. And here is the usage example. As I said - this is just an advice, but if you are in the very beginning of your authentication setup, I would recommend you to have a look.
In our developing e-commerce solution we are using AspNet Identity 2.2.1 and it is required that any guest (anonymous) users should complete checkout without prior registration to the website. In order to fullfill this requirement have written an ActionFilter named UserMigrationAttribute which obtains SessionTrackId (string GUID) from cookie -which we set from a HttpModule for every request if SessionTrackId is not found along with request cookies- and creates and actual IdentityUser in database with the username something like SessionTrackId#mydomain.com.
We have decorated our BaseController class with this UserMigration attribute in order to utilize its functions throughout the site.
Everything up to this point works as expected with single downside issue, which is when the page is being loaded for the first time for any user, if we try to make an Jquery Ajax Call to a Method which have [ValidateAntiForgeryToken] attribute, the call fails with the 'The provided anti-forgery token was meant for a different claims-based user than the current user.' error, even though we are sending __RequestVerificationToken parameter with every ajax call.
But if user opens another page by clicking link and/or reloads/refreshes current page, all the subsequent ajax calls complete successfully.
In our understanding UserMigrationAttribute creates user on OnActionExecuting method, but after we signIn user in the process #Html.AntiForgeryToken() is not being updated with the right values.
You may find the UserMigrationAttribute code below;
[AttributeUsage(AttributeTargets.Class)]
public class UserMigrationAttribute : ActionFilterAttribute
{
public ApplicationSignInManager SignInManager(ActionExecutingContext filterContext)
{
return filterContext.HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
}
public UserManager UserManager(ActionExecutingContext filterContext)
{
return filterContext.HttpContext.GetOwinContext().GetUserManager<UserManager>();
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
CreateMigrateCurrentUser(filterContext);
base.OnActionExecuting(filterContext);
}
private static readonly object LockThis = new object();
private void CreateMigrateCurrentUser(ActionExecutingContext filterContext)
{
lock (LockThis)
{
var signInManager = SignInManager(filterContext);
var userManager = UserManager(filterContext);
var sessionTrackId = GetSessionTrackId(filterContext);
if (!filterContext.HttpContext.Request.IsAuthenticated)
{
if (!string.IsNullOrEmpty(sessionTrackId))
{
var username = string.Format("{0}#mydomain.com", sessionTrackId);
var user = userManager.FindByName(username);
if (user == null)
{
user = new User() {UserName = username, Email = username};
var result = userManager.Create(user);
userManager.AddToRole(user.Id, StringResources.AnonymousVisitorsGroup);
}
signInManager.SignIn(user, true, true);
}
}
else
{
if (!string.IsNullOrEmpty(sessionTrackId))
{
var username = string.Format("{0}#mydomain.com", sessionTrackId);
var user = userManager.FindByName(username);
if (user != null)
{
if (!HttpContext.Current.User.IsInRole(StringResources.AnonymousVisitorsGroup))
{
var targetUserId = HttpContext.Current.User.Identity.GetUserId<int>();
var service = new Service();
service.Users.MigrateUser(user.Id, targetUserId);
}
}
}
}
}
}
private string GetSessionTrackId(ActionExecutingContext filterContext)
{
var retVal = string.Empty;
if (filterContext.HttpContext.Request.Cookies["stid"] != null)
{
retVal = filterContext.HttpContext.Request.Cookies["stid"].Value;
}
return retVal;
}
}
Any help or suggestions are highly appreciated.
Thank you,
This is happening because the anti-forgery token is set in a cookie, which will not be updated until the next request. If you're manually signing a user in, you should also issue a redirect (even if to the same page they were already headed to), simply to ensure that the cookie data is correct. This normally happens naturally, as the sign in form will redirect to the URL that needed authorization after the user is signed in, thus negating the problem. Since you're not redirecting currently, the data is out of sync.
However, I have to say that this seems like a very poor solution to this particular use case. Creating some sort of temporary-type user and signing that user in to handle guest checkout creates an unnecessary glut of useless data in your database, at best, and leads to bugs and other issues like this one you're experiencing, at worst.
I also run an ecommerce site, and the way we handled guest checkout is incredibly simplistic. The checkout data is just stored in the session (email, shipping/billing address, etc.). We build a view model to handle the actual checkout where the data necessary for submitting the sale comes either from the user object, if they're logged in, or these session variables, if they aren't. If the user is neither logged in, nor has the requisite session variables set, then they are redirected to the onboarding form where billing/shipping, etc. is collected.
For other aspects like maintaining an anonymous cart, we use a permanent cookie with the cart identifier. If the user ends up creating an account, we associate the anonymous cart with their user, and then remove the cookie. This ensures that their cart survives past the session timeout and things like closing the browser, even if they're anonymous.
In other words, in all these things, no user object is actually needed. If it's there (user is logged in), great, we'll use it. Otherwise, we collect and persist the requisite information for checkout via other means.
I am working on a cross platform web app using angular and webapi. The problem is when the angular app runs in a cordova container. To play nice with the rest of the applications on the device, I am required to use a plugin for SSO.. This plugin is what is causing me issues, because it does a few things. It intercepts all the http requests and adds a bearer token to the header, which is generated by a 3rd party Token provider, so I can't decode it, and overwrites any bearer token I have set in the header.It also seems to block cookies..
So it makes it a bit tricky when you can't send you own local credentials.
So I started with https://coding.abel.nu/2014/06/writing-an-owin-authentication-middleware/ and http://katanaproject.codeplex.com/SourceControl/latest#src/Microsoft.Owin.Security.OAuth/OAuthBearerAuthenticationHandler.cs
So I figured I should write my own middleware to take care of this; I thought since the standard oauth middleware can work without cookies, I should not have too hard a time getting my slightly different bearer token middleware to do it.. But that has not been the case... Writing my own middleware.. so I'm able to get the header, validate with the external token provider, but I can't actually sign in.
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
try
{
// Find token in default location
string requestToken = null;
string authorization = Request.Headers.Get("Authorization");
if (!string.IsNullOrEmpty(authorization))
{
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
requestToken = authorization.Substring("Bearer ".Length).Trim();
}
}
.... Take the Request token call other Server, verify token...
Also
public override async Task<bool> InvokeAsync()
{
var ticket = await this.AuthenticateAsync();
if(ticket != null)
{
this.Context.Authentication.SignIn(new AuthenticationProperties(), grantIdentity);
return false;
}
}
So in the end the SignIn does not cause a error or anything, but does not actually signin. As soon as I get to a controller action with an [Authorize] attribute, I get a 401. I not have any external cookies enabled. There is a high probability that I am on the wrong track or I am making it way too hard.
You are doing it way too hard.
Instead of creating your own bearer authentication middleware you should change the default OAuthBearerAuthenticationProvider.
Here is a sample for sending the token in the query string.
//in Startup class
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
Provider = new QueryStringOAuthBearerProvider(),
//your settings
});
//implementation
public class QueryStringOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
private const string AccessTokenQueryKey = "access_token";
public override Task RequestToken(OAuthRequestTokenContext context)
{
//check if token found in the default location - "Authorization: Bearer <token>" header
if (string.IsNullOrEmpty(context.Token))
{
var token = context.Request.Query.Get(AccessTokenQueryKey);
if (!string.IsNullOrEmpty(token))
{
context.Token = token;
}
}
return Task.FromResult<object>(null);
}
}
So… I ment to answer it earlier, but I was able to figure it out, without override the authorize attribute. I ended up looking at the source for the OWIN security code. The trick is, you really need 2 OWIN middleware components. One is the what I call (and I stole this from the owin source) the server middleware. The server middleware responds to the challenge and/or if you are feeling crazy generate local credentials for you. This middleware is also a PASSIVE middleware component. I won’t get in to generating the local credentials unless someone asks , because it’s a bit off point, but if someone thinks it will be helpful, I can update.
public class LowCalorieAuthenticationServerHandler : AuthenticationHandler<LowCalorieAuthenticationServerOptions>
{
//Important this needs to be overriden, but just calls the base.
protected override Task<AuthenticationTicket> AuthenticateCoreAsync()
{
return Task.FromResult<AuthenticationTicket>(null);
}
/// <summary>The apply response challenge async.</summary>
/// <returns>The <see cref="Task"/>.</returns>
protected override async Task ApplyResponseChallengeAsync()
{
if (this.Response.StatusCode != 401)
{
Task.FromResult<object>(null);
return;
}
var challenge = this.Helper.LookupChallenge(
this.Options.AuthenticationType,
this.Options.AuthenticationMode);
if (challenge != null)
{
//OK in here you call the rediret to the 3rd party
//return a redirect to some endpoint
}
Task.FromResult<object>(null);
return;
}
}
Anyway notice how the override AuthenticateCoreAsync() just returns
return Task.FromResult(null);
This is because we don’t want this middleware to modify the request. ApplyResponseChallengeAsync will wait for a Challenge and redirect you to the 3rd party login. IF you want to create a local token of some sort you would override the InvokeAsync method
The second middle ware you need is the token/external credentials validator. This will then authenticate the user somehow. In the case of the local bearer token that is built into the OWIN security, it simple deserializes the token and if it can, and the token is not expired it authenticates the user. So in the case that you want to verify the token with a 3rd part sso, such as google or anything, you insert you logic here. In my case I not only wanted to call the 3rd party provider to get the user info, but to check if they token was still valid for single sign out, and to prevent multiple sessions.
public class LowCalorieAuthenticationHandler : AuthenticationHandler<LowCalorieAuthenticationOptions>
{
//Going to give you the user for the request.. You Need to do 3 things here
//1. Get the user claim from teh request somehow, either froma header, request string, or cookie what ever you want
//2. validate the user with whatever user store or 3rd party SSO you want
//3. Generate a AuthenticationTicket to send to on to the request, you can use that to see if the user is valid in any Identity collection you want.
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
//Good to throw in a point of override here.. but to keep it simple-ish
string requestToken = null;
string authorization = Request.Headers.Get("Authorization");
//TOTAL FAKEOUT.. I am going to add a bearer token just so the simple sample works, but your client would have to provide this
authorization = "Bearer 1234567869";
//STEP 1
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
requestToken = authorization.Substring("Bearer ".Length).Trim();
return await FakeExternalBearer(requestToken);
}
return null;
}
private async Task<AuthenticationTicket> FakeExternalBearer(string token)
{
var authenticationType = Options.AuthenticationType;
//pretend to call extenal Resource server to get user //STEP 2
//CallExternal(token)
//Create the AuthTicket from the return.. I will fake it out
var identity = new ClaimsIdentity(
authenticationType,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier,"user1", null, authenticationType));
identity.AddClaim(new Claim(ClaimTypes.Name, "Jon",null, authenticationType));
var properties = new AuthenticationProperties();
properties.ExpiresUtc = DateTime.UtcNow.AddMinutes(1);
properties.IssuedUtc = DateTime.UtcNow;
var ticket = new AuthenticationTicket(identity, properties);
return ticket;
}
}
Ok here we override AuthenticateCoreAsync, but we actually do something now. This this were your do you user authentication. This is the ACTIVE part of the middleware. Note it needs to return a valid AuthenticationTicket. This will run on each request so be careful what you call and how often.
So I have a very simple example here https://github.com/jzoss/LowCalorieOwin If anyone is interested in more detail, please ask. I can add more. I did make it too hard, because now that I understand it, it’s pretty easy, but there is really no good examples on how to do this.
I am new to webapi and mvc and I am struggling to find a best practice for handling authorizations dynamically based on roles and ownership of the resource. For example an account page that should allow employee admins, employee call center or the owning client to Get, Post, Put or Delete account information. So an admin and call center employee should be able to Get, Post, Put or Delete any request for any userid, but a client should only be able to perform these actions on resources owned by them.
For example Tom is UserID 10 and Jerry is UserID 20.
/api/Account/10 should be accessible by any admin, call center or Tom. Jerry should be kicked out.
/api/Account/20 should be accessible by any admin, call center or Jerry. Tom should be kicked out.
In webforms the typical solution is to just check if the user is a client and verify their id against the request. (I know AuthorizeAttribute is not in webforms, but showing as an example of what it would covert to in webapi/mvc.)
[Authorize(Roles = "Administrator, CallCenter, Client")]
public string Get(int userID)
{
if (Thread.CurrentPrincipal.IsInRole("Client") && Thread.CurrentPrincipal.Identity.userID != userID)
{
//Kick them out of here.
}
return "value";
}
This will work, but it seems like the check for ownership should happen in a single location before it reaches the controller and should be reusable throughout an application. I am guessing the best place would either be a custom AuthorizationFilterAttribute or a custom AuthorizeAttribute and maybe create a new role ClientOwner.
[Authorize(Roles = "Administrator, CallCenter, ClientOwner")]
public string Get(int userID)
{
return "value";
}
Custom AuthorizeAttribute
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
//If user is already authenticated don't bother checking the header for credentials
if (Thread.CurrentPrincipal.Identity.IsAuthenticated) { return; }
var authHeader = actionContext.Request.Headers.Authorization;
if (authHeader != null)
{
if (authHeader.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase) &&
!String.IsNullOrWhiteSpace(authHeader.Parameter))
{
var credArray = GetCredentials(authHeader);
var userName = credArray[0];
var password = credArray[1];
//Add Authentication
if (true)
{
var currentPrincipal = new GenericPrincipal(new GenericIdentity(userName), null);
var user = GetUser(userName);
foreach (var claim in user.Cliams)
{
currentPrincipal.Identities.FirstOrDefault().AddClaim(new Claim(ClaimTypes.Role, claim);
}
//**************Not sure best way to get UserID below from url.***********************
if (user.userTypeID = UserTypeID.Client && user.userID == UserID)
{
currentPrincipal.Identities.FirstOrDefault().AddClaim(new Claim(ClaimTypes.Role, "ClientOwner"));
}
Thread.CurrentPrincipal = currentPrincipal;
return;
}
}
}
HandleUnauthorizedRequest(actionContext);
}}
Can someone point me in the right direction as to the best place to handle the authorization of the individual user? Should this still be done in the controller or should I move it to a custom AuthorizationFilterAttribute or a custom AuthorizationAttribute or is there somewhere else this should be handled? If the proper place is in a custom attribute, then what is the best way to get the userID and should I create a new role like the example above or should I do something different?
This is a common scenario and I am very surprised that I have struggled to find examples of the above scenario. This leads me to believe that either everyone is doing the check in the controller or there is another term I am not aware of so I am not getting good google results.
I think you may be getting authorization and permissions confused. "Dynamic authorization" isn't something you ever do.
Authorization is the act of verifying an author.
Request claims it is being sent from Alice.
Request presents a password or authorization token that proves the requester is Alice.
Server verifies that the password or authorization token matches its records for Alice.
Permissions are the business logic that specifies who can do what in your system.
Request is already authorized, and we know it came from Alice.
Alice is requesting to delete an important resource.
Is Alice an administrator? If not, tell her she can't do that because she doesn't have permission. (403 Forbidden)
The built-in [Authorize] attribute lets you optionally specify Roles that are permitted to access a resource. That option to specify permissions as part of authorization is slightly misplaced, in my opinion.
My advice would be to leave authorization as purely the process of verifying the author of a request. The BasicAuthHttpModule described here is close to what you want already.
Non-trivial permissions logic needs to be handled inside of your action body. Here's an example:
//Some authorization logic:
// Only let a request enter this action if the author of
// the request has been verified
[Authorize]
[HttpDelete]
[Route("resource/{id}")]
public IHttpActionResult Delete(Guid id)
{
var resourceOwner = GetResourceOwner(id);
//Some permissions logic:
// Only allow deletion of the resource if the
// user is both an admin and the owner.
if (!User.IsInRole("admin") || User.Identity.Name != resourceOwner)
{
return StatusCode(HttpStatusCode.Forbidden);
}
DeleteResource(id);
return StatusCode(HttpStatusCode.NoContent);
}
In this example, it would be difficult to convey the permissions logic as an attribute on the action, because the portion of the permissions that compares the current user to the resource owner can only be evaluated after you have actually gotten the resource owner info from your backend storage device.