问题
We currently have an API that requires multi-tenancy. Upon a successful login we issue all the available tenants to the user but when a user chooses to change tenant I'm starting to run into issues about in which tenant-context the users is attempting to access a resource.
My thought was that upon changing tenant I can just offer a ChallengeResult()
and then in IdentityServer handle this to check what tenant the user would like to access and then issue a new token with the claim "ChosenTenant" (or something like that). Like so:
var authProps = new AuthenticationProperties();
authProps.SetString("CurrentTenantId", response.Result.Id.ToString());
authProps.RedirectUri = "/#home";
return new ChallengeResult(authProps);
However this doesn't really work because ChallengeResult just hits the IdentityServers "/connect/authorize" endpoint and then is returned to my application. I have an implementation of IProfileService in our IdentityServer but I can't access my AuthenticationProperties previously issued.
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.FindFirst("sub").Value;
context.IssuedClaims = await _userStore.GetClaimsForUser(sub, context.RequestedClaimTypes, context.Client.ClientId);
return;
}
My question then is: how can I intercept or do some own internal logic when hitting /connect/authorize? Or maybe I'm going about this the wrong way?
回答1:
... but when a user chooses to change tenant I'm starting to run into issues about in which tenant-context the users is attempting to access a resource.
This really isn't a concern. Security is about protecting the resource. Selecting a tenant doesn't change the authorization of the user. Though not currently selected, the user remains authorized for all tenants that are configured for this user.
So it is perfectly alright to add the tenantid as parameter to the api call and then verify to see whether the user is allowed for this tenant. Don't confuse authorization with filtering.
From the Api point of view the only context there is, is the one provided and maintained by the client.
The client can keep track of the current tenant in various ways: cookie, session, in memory, route, e.g. /tenant/id/
or tenant.domain.com
.
回答2:
Since I host some API-functions in the Identity Server to do some internal user modifications I needed tenant to be reflected in the token so that the calls to this API could be validated.
I managed it by implementing a new custom grant in the Identity Server which looks at the raw context and pulls out the requested tenant.
public class ActAsGrantValidator : IExtensionGrantValidator
{
private readonly ITokenValidator _tokenValidator;
private readonly ITenantStore _tenantStore;
public ActAsGrantValidator(ITokenValidator tokenValidator, ITenantStore tenantStore)
{
_tokenValidator = tokenValidator;
_tenantStore = tenantStore;
}
public string GrantType => "act-as";
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var userToken = context.Request.Raw.Get("accessToken");
var tenant = context.Request.Raw.Get("chosenTenant");
if (string.IsNullOrEmpty(userToken))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var result = await _tokenValidator.ValidateAccessTokenAsync(userToken);
if (result.IsError)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
// logic to validate the user and tenant
// ..
// issue a new claimsprincipal to reflect the new "persona"
var claims = result.Claims.ToList();
claims.RemoveAll(p => p.Type == "role");
claims.RemoveAll(p => p.Type == "chosentenant");
claims.Add(new Claim("chosentenant", tenant));
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
context.Result = new GrantValidationResult(principal);
return;
}
}
Then in a controller from the API I call the custom grant:
// access token from httpcontext, chosenTenant from controller
var payload = new
{
accessToken,
chosenTenant
};
var tokenClient = new TokenClient($"{_identityServerSettings.Authority}/connect/token",
_identityServerSettings.ClientId, _identityServerSettings.ClientSecret);
var response = await tokenClient.RequestCustomGrantAsync("act-as", "profile openid", payload);
And then do a relogin with the claims from the new access token and calling the user info endpoint:
var authProperties = new AuthenticationProperties();
authProperties.StoreTokens(tokens);
authProperties.ExpiresUtc = DateTime.Now.AddHours(9);
var identity = new ClaimsIdentity(claims, "Cookies");
var principal = new ClaimsPrincipal(identity);
await _httpContext.HttpContext.SignInAsync("Cookies", principal, authProperties);
来源:https://stackoverflow.com/questions/56171770/issue-a-new-claim-from-challengeresult-in-identityserver4