How can I revoke Reference Tokens for blocking users? - c#

I have an implementation of Identity Server 4 that uses Entity Framework Core for persistent storage and ASP.NET Core Identity for users management. Since this IDS will support public applications, we were asked to add a way of completely blocking users - which means not allowing them to sign in and remove their existing logins.
After long research, I've determined that IDS does not support anything like expiring Access Tokens, since that's not part of OpenID Connect. What strikes me as completely odd is that I switched a client to use Reference Tokens, which are correctly stored in the PersistedGrants table, but even clearing that table doesn't invalidate future requests, as the user is still authenticated both to the client application and to Identity Server itself.
Is there any store/service I can re-implement to block all access from a given logged in user?

You'll have to clear the cookies as well.
But you may want to investigate a different approach. Where IdentityServer is used as an authentication service and authorization is outsourced, like the PolicyServer.
That way you can opt-in authorization, making it less important that a user is still authenticated.

In the end, the problem was that someone had changed AccessTokenType from 1 back to 0 since another API didn't work, as it was configured for using Access Tokens rather than Reference Tokens. Thanks #VidmantasBlazevicius for pointing in the right direction of looking at logs for calls to the connect/introspect endpoint.
For reference, this is the code we ended up using (called by admin users, appropriately secured):
[HttpPut("{userId}/Block")]
public async Task<IActionResult> BlockUser(string userId)
{
var user = await _context.Users.FindAsync(userId);
if (user == null || !user.LockoutEnabled || user.LockoutEndDate > DateTime.Now)
{
return BadRequest();
}
var currentTokens = await _context.PersistedGrants
.Where(x => x.SubjectId == user.UserId)
.ToArrayAsync();
_context.PersistedGrants.RemoveRange(currentTokens);
var newLockOutEndDate = DateTime.Now + TimeSpan.FromMinutes(_options.LockOutInMinutes);
user.LockoutEndDate = newLockOutEndDate;
string updater = User.Identity.Name;
user.UpdateTime(updater);
await _context.SaveChangesAsync();
return NoContent();
}

Related

How to add custom claims to Jwt Token in OpenIdConnect in .Net Core

Just like AzureAD we have our own custom Firm ActiveDirectory which we are connecting from UI as well as API for Authentication in .NetCore using OpenIdConnect (AddOpenIdConnect extension method).
In my use case after authentication on UI side, I need additional application specific claims from my custom database which I am adding "OnTokenValidated" - this is needed for hiding or exposing the UI elements based on Roles and Claims.
OnTokenValidated = async ctx =>
{
//Get user's immutable object id from claims that came from Azure AD
string oid = ctx.Principal.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier");
//Get EF context
var db = ctx.HttpContext.RequestServices.GetRequiredService<AuthorizationDbContext>();
//Check is user a super admin
bool isSuperAdmin = await db.SuperAdmins.AnyAsync(a => a.ObjectId == oid);
if (isSuperAdmin)
{
//Add claim if they are
var claims = new List<Claim>
{
new Claim(ClaimTypes.Role, "superadmin")
};
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
}
}
Now after token validation on API side again I have to call the custom database to fetch application specific roles. Is it possible to include these roles in JWT token itself so on API side all roles and claims(AD + Custom DB) are present. Or any other way by which I don't have to call CustomDB again in API.
Adding custom claims to access tokens is a capability of the Authorization Server (AS) and not all of them support this - though they should since it is an important feature. If you can say exactly what provider you are using I may be able to tell you whether it is possible.
These are the factors to think about:
Find out if the AS can reach out and get custom claims at the time of token issuance as in this Curity article.
Be careful to not return detailed JWTs to internet clients and aim to keep them within your back end instead. Opaque tokens can help with this as in this other Curity article
If custom claims are not possible for your provider then you can look them up in your API(s) when an access token is first received and then cache the claims in memory. This leads to more complex API code but is necessary for some providers.

MSAL - PublicClientApplication - GetAccountsAsync() doesn't return any Accounts

I'm developing a little WPF-App that is supposed to query some data from the MS Graph API. I want to use SSO, so the user doesn't have to login to the app seperatly.
The app is run on a Azure AD joined device. The user is an AADC synchronized AD user. The AAD tenant is federated with ADFS. The user authenticates with Hello for Business (PIN) or via Password. The resulting problem is the same.
I can confirm that the user got a PRT via:
dsregcmd /status
AzureAdPrt: YES
In case it matters: The app registration in Azure AD is set to "Treat application as public client". And the following redirect URIs are configured:
https://login.microsoftonline.com/common/oauth2/nativeclient
https://login.live.com/oauth20_desktop.srf
msalxxxxxxx(appId)://auth
urn:ietf:wg:oauth:2.0:oob
Based on the examples I found, I'm using the following code to try to get an access token. However the GetAccountsAsync() method doesn't return any users nor does it throw any error or exception.
Can anyone tell me, what I'm missing here?
Any help would be much appreciated!
PS: When I try this using "Interactive Authentication" it works fine.
public GraphAuthProvider(string appId, string tenantId, string[] scopes)
{
_scopes = scopes;
try
{
_msalClient = PublicClientApplicationBuilder
.Create(appId)
.WithAuthority(AadAuthorityAudience.AzureAdMyOrg, true)
.WithTenantId(tenantId)
.Build();
}
catch (Exception exception)
{
_log.Error(exception.Message);
_log.Error(exception.StackTrace);
throw;
}
}
public async Task<string> GetAccessToken()
{
_log.Info("Starting 'GetAccessToken'...");
var accounts = await _msalClient.GetAccountsAsync();
_userAccount = accounts.FirstOrDefault();
// If there is no saved user account, the user must sign-in
if (_userAccount == null)
{
_log.Info("No cached accounts found. Trying integrated authentication...");
[...]
}
else
{
// If there is an account, call AcquireTokenSilent
// By doing this, MSAL will refresh the token automatically if
// it is expired. Otherwise it returns the cached token.
var userAccountJson = await Task.Factory.StartNew(() => JsonConvert.SerializeObject(_userAccount));
_log.Info($"Found cached accounts. _userAccount is: {userAccountJson}");
var result = await _msalClient
.AcquireTokenSilent(_scopes, _userAccount)
.ExecuteAsync();
return result.AccessToken;
}
}
To be able to have IAccounts returned from MSAL (which access the cache), it must have the cache bootstrapped at some point. You are missing the starting point, which in your case is AcquireTokenInteractive.
It is recommended to use the following try/catch pattern on MSAL:
try
{
var accounts = await _msalClient.GetAccountsAsync();
// Try to acquire an access token from the cache. If an interaction is required, MsalUiRequiredException will be thrown.
result = await _msalClient.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
.ExecuteAsync();
}
catch (MsalUiRequiredException)
{
// Acquiring an access token interactively. MSAL will cache it so you can use AcquireTokenSilent on future calls.
result = await _msalClient.AcquireTokenInteractive(scopes)
.ExecuteAsync();
}
Use this try/catch pattern instead of you if/else logic and you will be good to go.
For further reference, there is this msal desktop samples which covers a bunch of common scenarios.
Update
If you are instantiating a new _msalClient on every action, then this explains why the other calls are not working. You can either have _msalClient as a static/singleton instance or implement a serialized token cache. Here is a cache example
As there are some questions regarding non-interactive authentication in the comments, this is how I finally got this working:
Use WAM and configure the builder like this:
var builder = PublicClientApplicationBuilder.Create(ClientId)
.WithAuthority($"{Instance}{TenantId}")
.WithDefaultRedirectUri()
.WithWindowsBroker(true);
Configure this redirect URI in the Azure App registration:
ms-appx-web://microsoft.aad.brokerplugin/{client_id}
A code example is available here
In-case anyone has a similar problem, I had an issue where both GetAccountAsync and GetAccountsAsync (the latter being now deprecated), were both sometimes returning null. All I needed to do was make sure all my authentication adjacent libraries were up to date.
I think there was an issue where the in-memory token caching wasn't always working as intended, which seems to be fixed by a simple update.

best practice or a standard way of managing my authenticated user data with claims data

I am wondering if there is a best practice or a standard way of managing my user data with claims data.
Scenario: A users logs in using a 3rd party and is redirected back to my application. All i have at this point is a ID that maps to a user profile in our database. A angular application will be making requests to the back end api.
So im wondering where to put the (lets say) roles and flags the user has in our database for restricting route access and other things.
I guess i could fetch it every request but i can see it adding some overhead.
So i figure i have a few options: add my data to a Session, add my data to the current user (ClaimsPrincipal) or use Caching. These all have trade offs. Session locks, caching also has sync issues, fetching every request has latency, ClaimsPrincipal stuff seems unruly ?
im using net framework 4.7
Link to the code https://github.com/ricardosaracino/SamlNet
public class UserHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var currentUser = HttpContext.Current.User as ClaimsPrincipal;
var claims = currentUser?.Claims;
var nameIdentifierClaim = claims?.FirstOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier);
var nameId = nameIdentifierClaim?.Value;
// if NOT cached or expired
// read from database
// set user in cache with nameid
// set request.Properties from cache
request.Properties["currentUser"] = new CurrentUser()
{
Id = Guid.NewGuid()
};
return base.SendAsync(request, cancellationToken).ContinueWith((task) =>
{
var a = request.Properties["currentUser"];
// if modified add back to cache
return task.Result;
});
}
}
I ended up using Owin with a Application Cookie and Claims. i was able to easily translate these to cookies for client state and use them for permissions.
Good answer here
Is claims based authorization appropriate for individual resources

User.Identity fluctuates between ClaimsIdentity and WindowsIdentity

I have an MVC site that allows logging in using both Forms login and Windows Authentication. I use a custom MembershipProvider that authenticated the users against Active Directory, the System.Web.Helpers AntiForgery class for CSRF protection, and Owin cookie authentication middle-ware.
During login, once a user has passed authentication against Active Directory, I do the following:
IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(StringConstants.ApplicationCookie);
var identity = new ClaimsIdentity(StringConstants.ApplicationCookie,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
if(HttpContext.Current.User.Identity is WindowsIdentity)
{
identity.AddClaims(((WindowsIdentity)HttpContext.Current.User.Identity).Claims);
}
else
{
identity.AddClaim(new Claim(ClaimTypes.Name, userData.Name));
}
identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "Active Directory"));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userData.userGuid));
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, identity);
My SignOut function looks like this:
IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(StringConstants.ApplicationCookie);
Logging in is performed via a jQuery.ajax request. On success, the Window.location is updated to the site's main page.
Logging in with both Forms and IntegratedWindowsAuthentication (IWA) works, but I've run into a problem when logging in with IWA. This is what happens:
The user selects IWA on the login page and hits the submit button. This is sent to the regular login action via an ajax request.
The site receives the request, sees the "use IWA" option and redirects to the relevant action. 302 response is sent.
The browser automatically handles the 302 response and calls the redirect target.
A filter sees that the request is headed to the IWA login action and that User.Identity.IsAuthenticated == false. 401 response is sent.
The browser automatically handles the 401 response. If the user has not authenticated using IWA in the browser yet, they get a popup to do so (default browser behavior). Once credentials have been received, the browser performs the same request with user credentials.
The site receives the authenticated request and impersonates the user to perform a check against Active Directory. If the user passes authentication, we finalize SignIn using the code above.
User is forwarded to the site's main page.
The site receives the request to load the main page. This is where things sometimes go awry.
The User.Identity at this point is of type WindowsIdentity with AuthenticationType set to Negotiate, and NOT as I would expect, the ClaimsIdentity created in the SignIn method above.
The site prepares the main page for the user by calling #AntiForgery.GetHtml() in the view. This is done to create a new AntiForgery token with the logged in user's details. The token is created with the WindowsIdentity
As the main page loads, ajax requests made to the server arrive with ClaimsIdentity! The first POST request to arrive therefore inevitably causes an AntiForgeryException where the anti-forgery token it sent is "for a different user".
Refreshing the page causes the main page to load with ClaimsIdentity and allows POST requests to function.
Second, related, problem: At any point after the refresh, once things are supposedly working properly, a POST request may arrive with WindowsIdentity and not with ClaimsIdentity, once again throwing an AntiForgeryException.
It is not any specific post request,
it is not after any specific amount of time (may be the first/second request, may be the hundredth),
it is not necessarily the first time that specific post request got called during that session.
I feel like I'm either missing something regarding the User.Identity or that I did something wrong in the log-in process... Any ideas?
Note: Setting AntiForgeryConfig.SuppressIdentityHeuristicChecks = true; allows the AntiForgery.Validate action to succeed whether WindowsIdentity or ClaimsIdentity are received, but as is stated on MSDN:
Use caution when setting this value. Using it improperly can open
security vulnerabilities in the application.
With no more explanation than that, I don't know what security vulnerabilities are actually being opened here, and am therefore loathe to use this as a solution.
Turns out the problem was the ClaimsPrincipal support multiple identities. If you are in a situation where you have multiple identities, it chooses one on its own. I don't know what determines the order of the identities in the IEnumerable but whatever it is, it apparently does necessarily result in a constant order over the life-cycle of a user's session.
As mentioned in the asp.net/Security git's Issues section, NTLM and cookie authentication #1467:
Identities contains both, the windows identity and the cookie identity.
and
It looks like with ClaimsPrincipals you can set a static Func<IEnumerable<ClaimsIdentity>, ClaimsIdentity> called PrimaryIdentitySelector which you can use in order to select the primary identity to work with.
To do this, create a static method with the signature:
static ClaimsIdentity MyPrimaryIdentitySelectorFunc(IEnumerable<ClaimsIdentity> identities)
This method will be used to go over the list of ClaimsIdentitys and select the one that you prefer.
Then, in your Global.asax.cs set this method as the PrimaryIdentitySelector, like so:
System.Security.Claims.ClaimsPrincipal.PrimaryIdentitySelector = MyPrimaryIdentitySelectorFunc;
My PrimaryIdentitySelector method ended up looking like this:
public static ClaimsIdentity PrimaryIdentitySelector(IEnumerable<ClaimsIdentity> identities)
{
//check for null (the default PIS also does this)
if (identities == null) throw new ArgumentNullException(nameof(identities));
//if there is only one, there is no need to check further
if (identities.Count() == 1) return identities.First();
//Prefer my cookie identity. I can recognize it by the IdentityProvider
//claim. This doesn't need to be a unique value, simply one that I know
//belongs to the cookie identity I created. AntiForgery will use this
//identity in the anti-CSRF check.
var primaryIdentity = identities.FirstOrDefault(identity => {
return identity.Claims.FirstOrDefault(c => {
return c.Type.Equals(StringConstants.ClaimTypes_IdentityProvider, StringComparison.Ordinal) &&
c.Value == StringConstants.Claim_IdentityProvider;
}) != null;
});
//if none found, default to the first identity
if (primaryIdentity == null) return identities.First();
return primaryIdentity;
}
[Edit]
Now, this turned out to not be enough, as the PrimaryIdentitySelector doesn't seem to run when there is only one Identity in the Identities list. This caused problems in the login page where sometimes the browser would pass a WindowsIdentity when loading the page but not pass it on the login request {exasperated sigh}. To solve this I ended up creating a ClaimsIdentity for the login page, then manually overwriting the the thread's Principal, as described in this SO question.
This creates a problem with Windows Authentication as OnAuthenticate will not send a 401 to request Windows Identity. To solve this you must sign out the Login identity. If the login fails, make sure to recreate the Login user. (You may also need to recreate a CSRF token)
I'm not sure if this will help, but this is how I've fixed this problem for me.
When I added Windows Authentication, it fluctuated between Windows and Claims identities. What I noticed is that GET requests get ClaimsIdentity but POST requests gets WindowsIdentity. It was very frustrating, and I've decided to debug and put a breakpoint to DefaultHttpContext.set_User. IISMiddleware was setting the User property, then I've noticed that it has an AutomaticAuthentication, default is true, that sets the User property. I changed that to false, so HttpContext.User became ClaimsPrincipal all the time, hurray.
Now my problem become how I can use Windows Authentication. Luckily, even if I set AutomaticAuthentication to false, IISMiddleware updates HttpContext.Features with the WindowsPrincipal, so var windowsUser = HttpContext.Features.Get<WindowsPrincipal>(); returns the Windows User on my SSO page.
Everything is working fine, and without a hitch, there are no fluctuations, no nothing. Forms base and Windows Authentication works together.
services.Configure<IISOptions>(opts =>
{
opts.AutomaticAuthentication = false;
});

WSFederation & Manual ClaimsIdentity with AppPool recycles

I have an application that needs to support both Federation (IdP-Initiated) as well as manual authentication (standard username/password form). As such I am using .NET v4.5 System.Identity to make the application claims aware.
The issue we are seeing in dev is that anytime an AppPool recycle happens (like a recompile) and we reload the page or take any other action we get an error trying to access any of our custom claims. It's as if the user is still authenticated, but our custom claims are totally gone. In order to keep working, we need to close all instances of the browser and login again. This can obviously happen in the wild and is something we cannot have happen (horrible end user experience).
Is there something we are doing wrong or a way that we can trap/detect this condition and force the user to log back in again?
Background
In the case of a manual login, we build a CustomClaimsIdentity instance that gets passed into a new ClaimsPrincipal which is then used to create a new SessionSecuirytToken instance and then written out as follows:
var claims = CustomClaimsAuthenticationManager.BuildClaimsList( user );
var identity = new UniversalIdentity( claims, AuthenticationTypes.Password );
var principal = new ClaimsPrincipal( identity );
var token = new SessionSecurityToken( principal, TimeSpan.FromMinutes( user.Customer.SessionExp ?? 120 ) );
var sam = FederatedAuthentication.SessionAuthenticationModule;
sam.WriteSessionTokenToCookie( token );
In the case of Idp-Initiated logins, we handle the FederatedAuthentication.WSFederationAuthenticationModule.SignedIn event and perform checks to validate the supplied claims as well as build up the custom claims our app adds to the identity the same way we do for manual authentication.
You can safely cast an IPrincipal to ClaimsPrincipal:
ClaimsPrincipal cp = (ClaimsPrincipal)Thread.CurrentPrincipal;

Categories

Resources