I am working on a little project for SharePoint 2010 that will allow users to authenticate with their OpenID (and maybe more later on).
How it currently works is I have a membership and role provider based on SQL. My login page uses DotNetOpenAuth to send an authenticationrequest to the OpenID endpoint. If I get a positive response I read the e-mail address from the OpenID persona and create a session for the MembershipUser that has that e-mail address (if I know the endpoint he's from). If that user doesn't exist, I create a new MembershipUser and store the endpoint in the comment property (so that you can't simply edit your own persona to log in as another person's e-mail address).
This works, but the problem is in creating the session. I currently do this with the following bit of code:
//authenticate
SecurityToken token = SPSecurityContext.SecurityTokenForFormsAuthentication(new Uri(SPContext.Current.Web.Url),userProvider,roleProvider,username,user.GetPassword());
SPFederationAuthenticationModule.Current.SetPrincipalAndWriteSessionToken(token);
But that bit only works if the membership provider doesn't encrypt passwords because, once encrypted, I can't just read the user's password and use it in that method.
I was hoping to find a way to just let a user log in but I can't seem to find a simple way of doing that.
Using
FormsAuthentication.SetAuthCookie(username, false);
Just creates an exception (I assume that's because that method is for standard FBA, and not claims-based FBA but I'm just guessing there. The built-in FBA login control with username and password does work, though.)
System.ArgumentException: Exception of type 'System.ArgumentException' was thrown. Parameter name: encodedValue
at Microsoft.SharePoint.Administration.Claims.SPClaimEncodingManager.DecodeClaimFromFormsSuffix(String encodedValue)
at Microsoft.SharePoint.Administration.Claims.SPClaimProviderManager.GetProviderUserKey(String encodedSuffix)
at Microsoft.SharePoint.SPGlobal.CreateSPRequestAndSetIdentity(SPSite site, String name, Boolean bNotGlobalAdminCode, String strUrl, Boolean bNotAddToContext, Byte[] UserToken, String userName, Boolean bIgnoreTokenTimeout, Boolean bAsAnonymous)
at Microsoft.SharePoint.SPWeb.InitializeSPRequest()
at Microsoft.SharePoint.WebControls.SPControl.EnsureSPWebRequest(SPWeb web)
at Microsoft.SharePoint.WebControls.SPControl.SPWebEnsureSPControl(HttpContext context)
at Microsoft.SharePoint.ApplicationRuntime.BaseApplication.Application_PreRequestHandlerExecute(Object sender, EventArgs e)
at Microsoft.SharePoint.ApplicationRuntime.SPRequestModule.PreRequestExecuteAppHandler(Object oSender, EventArgs ea)
at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
Long story short, I would like a way to create tokens for my members without having to use their password.
Does anyone have some experience in this, or an idea of how I can get what I want?
Thanks in advance.
In case someone else runs into the same situation, I'll say how I 'solved' the problem.
In a professional environment, you would most likely create your own membership provider / security token service so you can request tokens in your code without requiring the password of the user.
In my case (due to time constraints) I couldn't do that, so I took a simpler way out. Instead of using encrypted passwords (via IIS) I used hashed passwords (set that manually in the web.config). I also made sure to allow resetting of passwords for my membership provider. Then I used the following code to issue a token:
MembershipUser user = membershipProvider.GetUser(username, true);
if(user == null)
{
//create new user here
}
string password = user.ResetPassword();
SecurityToken token = SPSecurityContext.SecurityTokenForFormsAuthentication(new Uri(SPContext.Current.Web.Url), membershipProviderName, roleProviderName, user.UserName, password, false);
SPFederationAuthenticationModule.Current.SetPrincipalAndWriteSessionToken(token);
Practically, this means that the forms user has his password reset each time I log him in, but since he never actually uses his password it's not that big of an issue. It works well enough to quickly show OpenID support in SharePoint.
Related
Question: How can I enforce existing users to set up 2FA in .Net Core 3.1 Identity?
I have seen a couple of answers here already, but I have issues with them as follows:
Redirect user to set up 2FA page on login if they do not have it set up. Problem with this is that the user can simply jump to a different url to avoid this, therefore it is not actually enforced.
Have some on executing filter that checks if the user has 2FA enbaled or not and if not redirect them to MFA set up page. The issue I have with this is that on every single navigation the server must go to the database to check whether the user has this field enabled, thus creating a significant performance hit on each request. I know one trip to the database may not sound like much but I have worked with applications where this was the norm and other things used this method, causing a pile up of pre action db queries. I want to avoid this kind of behavior unless absolutely necessary.
My current idea is to on login:
Check the users credentials but NOT log them in
userManager.CheckPasswordAsync(....)
If the credentials pass, check if the user has 2FA enabled or not. If they do, continue through login flow, if not:
Generate a user token:
userManager.GenerateUserTokenAsync(.......)
and store this along with the username in a server side cache. Then pass a key to the cached items with a redirect to the 2FA setup page, which will not have the [authorize] attribute set, allowing users not logged in to access it.
Before doing anything on the 2FA set up page, retrieve the cached items with the provied key andverify the token and username:
userManager.VerifyUserTokenAsync(......)
If this doesn't pass, return Unauthorized otherwise continue and get the current user from the supplied UserName in the url that was passed via a cache key. Also dump the cached items and key so that should the url be snatched by a dodgy browser extension it can't be used again.
Continue to pass a new cache key to new user tokens and usernames to each 2FA page to authenticate the user as they navigate.
Is this an appropriate use of user tokens? And is this approach secure enough? I'm concerned that having the user not logged in presents security issues, but I think it is necessary in order to avoid the previously mention problem of going to the database on every request to check 2FA, as with this method trying to navigate away will just redirect to login.
I implemented this via a Filter Method
I have a BasePageModel which all my pages inherit
public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
if (!User.Identity.IsAuthenticated)
{
await next.Invoke();
return;
}
var user = await UserManager.GetUserAsync(User);
var allowedPages = new List<string>
{
"Areas_Identity_Pages_Account_ConfirmEmail",
"Areas_Identity_Pages_Account_ConfirmEmailChange",
"Areas_Identity_Pages_Account_Logout",
"Areas_Identity_Pages_Account_Manage_EnableAuthenticator",
"Areas_Identity_Pages_Account_ResetPassword",
"Pages_AllowedPageX",
"Pages_AllowedPageY",
"Pages_Privacy"
};
var page = context.ActionDescriptor.PageTypeInfo.Name;
if (!user.TwoFactorEnabled && allowedPages.All(p => p != page))
{
context.Result = RedirectToPage("/Account/Manage/EnableAuthenticator", new { area = "Identity" });
}
else
{
await next.Invoke();
}
}
I then changed both the Disable2fa and ResetAuthenticator pages to redirect to the main 2fa page
public IActionResult OnGet() => RedirectToPage("./TwoFactorAuthentication");
And removed the reset/disable links from that page
I chose to implement a more modern and OAuth friendly solution (which is inline with .Net Core Identity).
Firstly, I created a custom claims principal factory that extends UserClaimsPrincipalFactory.
This allows us to add claims to the user when the runtime user object is built (I'm sorry I don't know the official name for this, but its the same thing as the User property you see on controllers).
In here I added a claim 'amr' (which is the standard name for authentication method as described in RFC 8176). That will either be set to pwd or mfa depending on whether they simply used a password or are set up with mfa.
Next, I added a custom authorize attribute that checks for this claim. If the claim is set to pwd, the authorization handler fails. This attribute is then set on all controllers that aren't to do with MFA, that way the user can still get in to set up MFA, but nothing else.
The only downside with this technique is the dev needs to remember to add that attribute to every non MFA controller, but aside from that, it works quite well as the claims are stored in the users' cookie (which isn't modifiable), so the performance hit is very small.
Hope this helps someone else, and this is what I read as a base for my solution:
https://damienbod.com/2019/12/16/force-asp-net-core-openid-connect-client-to-require-mfa/
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/mfa?view=aspnetcore-5.0#force-aspnet-core-openid-connect-client-to-require-mfa
I'm currently writing a Xamarin application and I wish to login to my app, this one use IdentityServer 4 for auth.
I wish to avoid any "web" interface for the email/password login, but I'm not sure how to do this (If possible).
So, for now I have a controller with a method like this:
public void LoginAsync(string cliendId, string clientSecret, string email, string password)
{
// Check the clientId & secrets match
....
// Sign in the user
var result = await SignInManager.PasswordSignInAsync(email, password, false, true);
// How to return a token to the Xamarin client here?
}
So, I have 2 questions please:
first I directly check the clientId & secret, is it the way to go? Or should I get a token, then protect this API, and then access it with the token.
how can I generate a new token for the Xamarin client to access the other APIs?
Thanks
Using OpenID Connect you are supposed to open a new web interface and let the user login through that page. Creating your own login/password form is a bad for many reasons. One is that the user does not now where his credentials will end up. Your application should never touch or see the users username/password.
If there is no user involved, then you can look at using the client credentials flow that is supported by IdentityServer.
Following on from this question, I have an interesting dilemma.
I've built out my IdentityServer based on the IdentityServer4 Quickstarts. When the Client directs the user to the IdentityServer, and the user authenticates, the AccountController on IdentityServer does a _signInManager.PasswordSignInAsync, and the user is signed in to IdentityServer.
This can be confirmed by opening another browser tab and navigating to the IdentityServer root URL, and sure enough, the logged-in user name shows on the top right corner, as expected.
As explained in my question linked above, the user is then directed to a page from which they need to select the Tenant they wish to work on, before being redirected back to the Client app (that interaction works).
However, following the selection of a Tenant, the TenantController then calls HttpContext.SignInAsync([the currently logged-in user's subject claim value], [the selected tenant claim]), with the intention that this re-signs-in the already-signed-in user, passing the additional Tenant Claim. (This is my attempt to get the selected TenantId claim to appear in the token sent back to the Client...and I'm open to suggestions about a better way to do this.)
And although the completion of this interaction does indeed pass back the expected user information in the token sent to the Client, it effectively signs the user out of IdentityServer?!? A refresh of the identity server root url in the other tab shows that no user is signed in anymore.
Why is this? What am I doing wrong? I need the user to remain signed in to the identity server as he/she was before selecting a Tenant.
I replaced the use of the IdentityServer4-provided extension of HttpContext.SignInAsync with the standard built-in (to Microsoft.AspNetCore.Authentication) HttpContext.SignInAsync.
So instead of:
public static async Task SignInAsync(this HttpContext context, string subject, params Claim[] claims)
from IdentitySever4, I used
public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
from Microsoft.AspNetCore.Authentication.
And in order to call it I built up the Principal as follows:
var userId = User.Claims.Single(r => r.Type == "sub").Value;
var user = await _userManager.FindByIdAsync(userId);
var principal = await _claimsFactory.CreateAsync(user);
((ClaimsIdentity)principal.Identity).AddClaim(new Claim("TenantId", tenant.Id.ToString()));
While configuring my IdentityServer4 (using Identity) resource owner grant flow with an asp.net core API backend, I got to thinking that perhaps the "Name" claim should remain omitted in the JWT access token for user security? This claim is not available with out of the box behavior of IS4.
Previously, I had been adding in the "Name" claim for the access token in my IS4 Config.cs file as follows:
var claims = new List<string>
{
JwtClaimTypes.Name
};
return new List<ApiResource>
{
new ApiResource("api1", "Auth API", claims)
};
I was doing this because it allows a straightforward approach to get a logged in user's ClaimsPrincipal.Identity.Name for user look up inside a Controller action.
var name = User.Identity.Name;
var user = await _userManager.FindByNameAsync(name);
However, IS4 access tokens (when using Identity) include the user's GUID id in the "Sub" claim. With this, we can also look up a user using the following:
var userId = User.Claims.FirstOrDefault(u => u.Type == "sub").Value;
var user = await _userManager.FindByIdAsync(userId);
I know there is slightly more processing with the LINQ query (hardly anything tbh), but I was thinking it might be of worth to protect a user's username (email address in my situation) if an access token ever fell into the wrong hands. Especially since JWT's are so easy to decode with the likes of jwt.io.
Do you guys agree or disagree? Or am I looking at this the wrong way and missing something?
JWT usually contain the public data and it is self-contained. i.e. You don't need to communicate with a backend server to construct user's identity. You should prevent the token fell into wrong hand by using https. Also, you should balance your token validity window(usability vs security) and use a nonce for maximizing the security.
I don't think 'name' should be omitted from claim collection. A valid use-case for what you are doing is that you need to make sure that changes to your user store immediately reflect in your web API. In the case of a self-contained token, if you change the 'name' in the data store, the user will not see that change until he was issued a new token. In this case use of a 'reference token' might be a good option.
Also, It looks like you are directly accessing user store from the web API. While you might have valid reasoning behind this, Idea of using token based authentication is to delegate authentication to external party(Identity Server). So common pattern is to
Include every public data that you require in the web API in the
access token.
If token getting too big, include a subset of claims in the token and query user info endpoint when required.
Use reference tokens if you have valid reasons to do so. But this will affect the performance as it will require back channel communication with identity server.
I am trying to implement IdentityServer 4 for enterprise scenario.
I understand that users are registered against Identity server.
My question is how to give permissions to users against applications, like as users are needed to assign to a particular application, if not assigned application should return unauthorized.
If a user needs to access multiple applications then multiple assignments are needed.
I am looking a way for Identity server to invalidate the submitted token if the user doesn't have access to the application in a single go, even though the challenged token might be valid if it is submitted by other application which the user has access to
Identity Server absolutely handles authorizations on the most basic level. It creates authorization codes and access_tokens that are essential in an applications authorization. Without them you cannot get authorized. Thus for others to claim Identity Server does not do authorizations is flat out wrong.
I came in here a week ago looking for a solution for this very same problem. I want to restrict users to specific applications by not granting them access tokens if they fail to meet certain parameters, in my case a UserClient table. Lucky for you I have a solution. Identity Server 4 implements a few, what they call, CustomValidators that occur at the time of authorization or token creation. They are
internal class DefaultCustomAuthorizeRequestValidator : ICustomAuthorizeRequestValidator
internal class DefaultCustomTokenRequestValidator : ICustomTokenRequestValidator
public class DefaultCustomTokenValidator : ICustomTokenValidator
There name really says it when they get called. Each one contains a single method
public Task ValidateAsync(CustomAuthorizeRequestValidationContext context)
{
return Task.CompletedTask;
}
Notice something? That's is right! It does nothing. Almost as if they are meant to be replaced. (It is).
This is the area that you can add your custom logic to reject the request. CustomAuthorizeRequestValidationContext contains ClientId and User claim information. It also contains a boolean value called IsError. Simply set that to true and whamy! Access denied. You can also set error messages etc. Here is an example that implements the ICustomAuthorizeRequestValidator inface that will restrict a user based on there user Id
public Task ValidateAsync(CustomAuthorizeRequestValidationContext context)
{
var sub = context.Result.ValidatedRequest.Subject.FindFirst("sub");
if (sub != null && sub.Value != "88421113")
{
context.Result.IsError = true;
context.Result.Error = "Unauthorized";
context.Result.ErrorDescription = "You are not authorized for this client";
}
return Task.CompletedTask;
}
Feel free to inject a dbcontext or two to read off of your userclient table. I check the sub claim to be null because this will get hit several times before actual login occurs.
From what I noticed all three behave similar in terms of use, but different in terms of outcome. Setting an error ICustomAuthorizeRequestValidator will prevent the redirect to your client and instead direct you to the Identity Server error screen. The other two will redirect back to the client and generally throw some throw some sort of HttpResponse error. Therefore replacing the ICustomAuthorizeRequestValidator seems to work best.
So simply created a class that implements ICustomAuthorizeRequestValidator. Then add that into your identity services like so
services.AddIdentityServer().AddCustomAuthorizeRequestValidator<MyCustomValidator>()
and you are done done.
You can add a claim in your IdentityServer4's claims table called "role" and in your application, add some UI to authorize a person via email or similar, and then set his/her role in the claims db. And you can also delete the authorized user from your application, which should un-assign a role to that particular person. Thus he/she although is successfully authenticated, can't use your application because you have authorized then. Hope this approach helps you!
For users, IdentityServer is authentication only. Authorization should be handled by your application.
Authentication = Verifying who a user is
Authorization = Verify what a user can do
Update
I wrote an article on this topic to clarify how OAuth 2.0 does is not user-level authorization. Hope it helps! https://www.scottbrady91.com/OAuth/OAuth-is-Not-User-Authorization
As Scott says, Identity Server will authenticate that the user is who they say they are, not explicitly tell you what that user can do.
You can use the claims returned as part of that authentication to then perform authorization checks within your app. For example, you might use the sub or id claims to perform checks from your app on whether the user associated with that sub/id is allowed to access a specific resource.
The water gets a bit muddier when you bring role claims into the picture, but so long as you appreciate the difference between authentication and authorization you should be ok.
In our enterprise scenario we split it into layers:
We introduced a tenant -- a customer (organization) of our enterprise
solution.
Then we have roles (not more than 20 or so) assigned for
each particular user.
IdentityServer fetches users from tenant and access APIs. The only pre-check it performs is that a particular client (application), requested a token, is not restricted for the particular tenant (customer-level licensing), otherwise we display a message and block the challenge response.
Then we come to an app. With a valid token, having tenant and roles inside. The roles-to-functions assignment could be unique within the tenant. So the application itself performs a granulate permissions check, using a separate API. The application is free to enable-disable some functions or even redirect to the special page in IdSrv "Access denied for the app".
With such approach we are scalable, we are configurable, we are as fast as we want. In previous generation we had "all in one" identity+access+licensing monster-like system, and we decided to split. Today we do not face any real limits with adding new customers (tenants), having 20000 users in average each.
Another way, you can redirect user back to respective client login page it they are not assigned to application/client by using IProfileService of IdentityServer4.Services
public async Task IsActiveAsync(IsActiveContext context)
{
if (!string.Equals("MyAllowedApplicationId", context.Client.ClientId, StringComparison.OrdinalIgnoreCase))
{
context.IsActive = false;
}
}
You have to set IsActive = false to redirect user back to login page where user can login with user details which is allowed in application