I am having trouble creating a custom authenticator for ADFS v4 on Windows Server 2019. My goal is to create a custom primary authenticator but right now I'd settle for getting a custom authenticator to work as an additional authentication provider. I followed this article by Microsoft and although it states the tutorial is for 2012, it's supposed to work for 2019 as well. I apologize if what follows comes across as a stream-of-consciousness but I'm fairly new to this and may have multiple things wrong with my implementation.
Initial struggles
When I follow the directions from Microsoft, I'm able to see the authenticator in the list of primary authenticators and select it. However, when I go through my authentication process the code never fires. I am never presented with the custom HTML fragment in the project. If I understand the code from this example correctly, I should be able to set the authenticator as primary and only get the HTML from my authenticator. The best I am able to do is get the friendly name to show up in a list of possible authenticators if more than one primary authenticator is selected.
/// Returns a Dictionary containing the set of localized friendly names of the provider, indexed by lcid.
/// These Friendly Names are displayed in the "choice page" offered to the user when there is more than
/// one secondary authentication provider available.
public Dictionary<int, string> FriendlyNames
{
get
{
Dictionary<int, string> _friendlyNames = new Dictionary<int, string>();
_friendlyNames.Add(new CultureInfo("en-us").LCID, "Matt's Friendly name in the Meatadata Class");
_friendlyNames.Add(new CultureInfo("fr").LCID, "Friendly name translated to fr locale");
return _friendlyNames;
}
}
If I click my custom authenticator it just errors and I get an entry in the ADFS event log that says it cannot find the specified user. I thought that by using the custom it would bypass any Active Directory lookup but apparently it's still doing the lookup and I'm never presented with my custom login page.
Then I added logging
I logged every method in my code to the Windows event log and for a little bit I would get a message that the program entered the OnAuthenticationPipelineLoad method.
public void OnAuthenticationPipelineLoad(IAuthenticationMethodConfigData configData)
{
//this is where AD FS passes us the config data, if such data was supplied at registration of the adapter
myNewLog.WriteEntry("Matt -> this is where AD FS passes us the config data, if such data was supplied at registration of the adapter");
}
Unfortunately, this stopped working at some point and I can't get it back so it's like the code isn't even making it to here any more.
Microsoft's example doesn't even work
I scrounged GitHub looking for other people who had done this and found Microsoft's example provider. Unfortunately, Microsoft's code doesn't work either so it must be something that I have configured wrong but I don't know where to look.
Then I tried making it the secondary authenticator
I tried setting my custom authenticator as secondary but the code never fires in this case either.
Suspicions
Before my logging stopped working, I thought the code might have an issue with the AuthenticationMethods metadata.
/// Returns an array of strings containing URIs indicating the set of authentication methods implemented by the adapter
/// AD FS requires that, if authentication is successful, the method actually employed will be returned by the
/// final call to TryEndAuthentication(). If no authentication method is returned, or the method returned is not
/// one of the methods listed in this property, the authentication attempt will fail.
public virtual string[] AuthenticationMethods
{
get {
myNewLog.WriteEntry("Matt -> AuthenticationMethods");
return new[] { "https://example.com/myauthenticationmethod1" }; }
}
I found hints that this could be an issue here and here. It says "IAuthenticationAdapterMetadata: defines adapter metadata including its name and the type(s) of authentication it supports" and "Both the Global and Relying Party MFA AdditionalAuthenticationRules claim rule sets are executed. (Box C). If the output claim set of either rule set contains a claim of type "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod" and value "http://schemas.microsoft.com/claims/multipleauthn", then MFA will engage."
Question
I guess I don't know what I should even ask as my question. Has anyone created a custom ADFS authenticator before and seen this problem? Is there something obvious that I can check that could be causing this?
This turned out to be a two-part problem and was party caused by my lack of domain knowledge.
Secondary authenticator fix
My issues with using my code as a secondary authenticator had to do with setting the claims rules. This is the power shell script that the tutorial had me run:
Set-AdfsRelyingPartyTrust –TargetRelyingParty $rp –AdditionalAuthenticationRules 'c:[type == "http://schemas.microsoft.com/ws/2012/01/insidecorporatenetwork", value == "false"] => issue(type = "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod", value = "http://schemas.microsoft.com/claims/multipleauthn" );'
My problem was that I was testing from inside my network as opposed to outside my network so the script was wrong for me. Once I edited the rule to force MFA for internal users, my code successfully hit.
Set-AdfsRelyingPartyTrust –TargetRelyingParty $rp –AdditionalAuthenticationRules 'c:[type == "http://schemas.microsoft.com/ws/2012/01/insidecorporatenetwork", value == "true"] => issue(type = "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod", value = "http://schemas.microsoft.com/claims/multipleauthn" );'
Primary authenticator fix
As it turns out, I cannot use my code as a primary authenticator in the way I was trying to. ADFS only allows you to authenticate against the AD identity provider. The message "cannot find the specified user" in the ADFS event log was a result of trying to bypass the step that sets the user. Since I did not want to query Active Directory for my user, I would then get an error that the authenticator could not find the specified user...makes sense.
What I actually needed was a different identity provider. For my relying party trust in ADFS, I needed to specify an identity provider other than Active Directory. I found an example of this here. This solution uses identity server 3 hosted in an asp.net site as an identity provider. By using this solution, I was able to manually set the claims for the current user.
This solution uses identity server 3 which is outdated and ideally you would use the currently supported identity server 4. However, the ws-federation part of identity server 4 has moved behind a pay wall.
I still had a problem with my relying party trust since they would be presented with multiple identity providers (or claims providers as ADFS calls them) when users came to our site from the relying party. To prevent this screen from showing up, you can set your identity server as the default claims provider for your relying party.
Set-AdfsRelyingPartyTrust -TargetName "Relying Party" -ClaimsProviderName ("IdentityServer")
Additional notes
There is a good write on what identity server is and the problem it is trying to solve here
There is also an open source repo for filling in the ws-federation piece of identity server 4 here which I did not use
Related
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
Here is my development environment:
Intranet Website
Active Directory Authentication/Authorization
Asp Net Core
I am trying to get the data stored in Active Directory attributes when a user enters firstly to any page in our application. All users rights and permissions, employeeid, studentid, etc.... are stored in AD Attributes and Security Groups. Some Attributes need to be displayed on the website too.
Let's say my website got the following urls...
http://mysite/Home/Index
http://mysite/Student/Index
http://mysite/Student/MyJobs
http://mysite/Staff/Applications
etc....
Any users can go onto some areas/urls of the website freely from other Intranet portals and I don't know where should I write the code to fulfill that criteria. The problem is that, there is no specific entry point to the application like http://mysite/Login or Authenticate, etc. If there is, I could load all users details and rights from AD on that single entry point.
In MVC5 era, I used Custom Global Authorize Attribute and put it on the BaseController is inherited from all other controllers to load that AD data. I put the AD's data into Session on the first hit and use the Static Class to display on Views and use in Controllers. But when I did some research in MVC Core, some say that it's outdated and I should use the Authorize Policy instead of custom Authorize Attributes.
Getting the data from Active Directory is already achieved by using my old webservices and we don't need to worry about .Net core not supporting AD yet.
I looked at the tutorials about Policy and saw something about Claims and Custom User Managers. I couldn't decide which one I should use to load data from Active Directory to the object (probably Scoped Object DI) which lasts for the whole user's session.
Should I load the data onto claims attributes
Eg...
var claims = new List<Claim>();
claims.Add(new Claim("UserName", "John.Smith", ClaimValueTypes.String, Issuer));
claims.Add(new Claim("RefNo", "02343001", ClaimValueTypes.String, Issuer));
claims.Add(new Claim("Email", "MyEmail#email.com", ClaimValueTypes.String, Issuer));
Or Should I write customized SignInManager and IdentityUser?
Eg...
public class ApplicationUser : IdentityUser
{
public string RefNo { get; set; }
public string Email { get; set; }
}
Is there anywhere I could put my code to check AD and load data?
And should I store the data in that Claimed Object rather than using Session Data?
Could you guys please advise me? Feel free to criticize if I miss anything and my idea is not working.
You're right in saying there's no System.DirectoryServices yet (it's on the backlog, I promise) so there are a couple of places to do this.
If you're already using Integrated Authentication you have SIDs for group membership, which are resolved when you call IsInRole(), so you can use role based membership (rather than Claims based) to solve basic authentication problems.
However if you want to support a forms based mechanism then you should look at using the cookie middleware, raw, to at least give you a simple login, calling your web service to validate your login. You could query your API in the controller code, and write an identity cookie. This cookie automatically encrypted and signed, so it can't be tampered with.
The problem comes when you want roles, and attributes. If you head down the cookie route you might be tempted to put all of those as claims in the identity before writing the identity out as a cookie. This might work, provided there are not too many - cookies have a maximum size (browser dependent, but under 4k usually). You can used chunked cookies, but there's a performance impact here. Instead you might use a reference cookie, where you put in a reference to another store where the actual fully populated identity is stored, be it session, redis or something else.
Then in the claims transformation middleware you can pull the reference out, go to your store, and rehydrate the identity.
I'd honestly avoid trying to merge all of this into ASP.NET Identity. That's mean to be the sole source for user information in an application, and in your case that's not true. Your sole source should be AD.
There's also a port of Novell's ldap library to core, which should stand in nicely for DirectoryServices should you want to avoid your web services approach.
Has anyone successfully implemented the Google Identity Toolkit, an implementation of an Account Chooser. I followed the initial steps here, but I still have a few questions, as I don't quite know how to handle the entire data flow. I'm using Clojure / Compojure in the back-end:
http://havethunk.wordpress.com/2011/08/10/google-identity-toolkit-asp-net-mvc3/
http://code.google.com/apis/identitytoolkit/v1/acguide.html
A) don't quite understand how ID Provider authentication, fits into my data model
when implementing the callbackURL, what data should I expect, and
how's that session state managed by GITkit (and all Account Choosers)
B) Is there a way to set this up the 'callbackURL' for development.
the identity provider would need a URL that it can redirect back to
C) How can the GITkit / Account Chooser workflow let my users register an account that's native to my app?
Thanks in advance
The questions aren't entirely clear, but I've done an implementation of GITkit in ruby and can give you some pointers.
A) The callback URL is what handles the assertion from the identity providers. Rightnow GITKit only does OpenID, so the URL will contain an OpenID response either in the query parameters or as the POST body. You'll need to do a few things:
1) Call verifyAssertion in the gitkit API and pass the params/post body. This will return a JSON response that contains the user details (assuming assertion is valid). There are some other checks you should do as well
2) Decide what to do with the assertion. If it is an existing user, most likely you'll just establish a session and save the user ID. If it's a new user, you can either create a new account and start a session immediately, or defer that and redirect them to a signup page.
3) Render HTML/JS to notify the widget. There are different status codes and data you can return that changes the flow.
GITKit itself doesn't really manage session state, that's up to your app. Some of the reference implementations have code to help, but it's not part of the API. The widget does have some state that you can control with JS (add account, show as logged in, etc) and uses local storage in the browser.
The docs give some details and example code for how this should be implemented.
B) Of course. The URL is just configured in the javascript widget when you call setConfig() It can be set to localhost or any staging server for development. So long as your browser can reach it you're OK.
C) By "native", I assume you mean where they're signing up with just a username/password instead of using an IDP. If so, the user just has to enter their email address when logging in. If that email address matches a known IDP it'll attempt to authenticate with OpenID, otherwise if it's a new user it'll redirect to whatever signup page you configured in the widget. That signup page would just ask the user to create a password like you normally would. You should also return whether or not accounts are 'legacy' (password) accounts in the userStatus checks.
Hope that helps.
For anyone's future reference. I was able to resolve the issue. You can follow this thread of how's it's done in Clojure.
I got it working with Ring/Compojure, and another fellow showed me his solution in Webnoir.
HTH
OK, so I'm quite new to the whole world of claims aware applications. I was able to get up and running very quickly using Azure ACS but it's been a bit of a different story when trying to use ADFS 2.0 as the identity provider (I want to actually use it as a federated provider, but for the time being I'm just trying to get a sample running using it as an identity provider).
I've been looking at the guides here and have tried to follow the AD FS 2.0 Federation with a WIF Application Step-by-Step Guide guide listed there. It takes you through setting up ADFS 2.0 along with a little claims aware sample application that you can use just to view the claims that are getting sent through.
So I can get that up and running, passing through the claims defined in the guide (just the windows account name). The problem is when I try to add any more. I can go to the relying party application in the ADFS GUI and add an Issuance Transform Rule, using the Pass Through or Filter Incoming Claim rule template. However, when I run my application, unless the added claim type is Name, it won't pass the claim through to my application.
One of the ones that I wanted passed through was the email address for the user who logged in to the application. So I added a rule to pass through the email address, then updated the web.config of the sample application to uncomment this line under the claimTypeRequired section:
<claimType type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" optional="false" />
Note that I'm setting it as non-optional. I also updated the federation metadata of the application to add in the following:
<auth:ClaimType Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" Optional="false" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" />
I then went into the ADFS GUI, went to the Relying Party Trusts and selected Update from Federation Metadata on my sample application. So it now lists the email as one of the accepted claims.
I then went into the Claims Provider Trusts and added the email claim rule into the Acceptance Transform Rules for the Active Directory provider trust (the only one listed).
When I run the app however, it's not passing through the email claim (or any others that I try). Can somebody tell me what I'm missing here?
I should also note, I ran a test to change my application to only accept the email claim rule, and not only did it not pass through the email, but it's still passing through the Windows Account Name and the Name claims, despite the fact that I don't even list them as accepted claims for my application.
If anybody could point out where I'm going drastically wrong here, it would be seriously appreciated.
After enabling logging as per the blog post before, here are the relevant entries from the log:
Event ID 1000, "Input claims of calling principal included in details":
So you can see, the information that I'm requesting is quite clearly missing. I have the logging output set to verbose but there's really nothing of any other interest. You'll see trace records for the NETWORK SERVICE user (with the same set of claims), but nothing striking. All the log entries are informational, there aren't any errors.
If you using ADFS as Identity Provider and want it to issue an email claim, then you have to use Send LDAP Attributes as Claims or a Custom Claim Rule which access AD as the attribute store and issues an email claim. Pass through is used on the incoming claims, assuming the user is already authenticated somewhere. In case of Windows Authentication Windows account name is issued from the Kerberos token and that's why you have to pass it through, but others you have to issue.
Does Active Directory issue E-Mail Address claims? I'm not sure how to check this, but if it doesn't, it's irrelevant that you're passing them through. In this case, you'll want to try a "Send LDAP Attributes as Claims" rule; based on what I see in my ADFS instance, try mapping the "E-Mail-Addresses" attribute to an "E-Mail Address" claim.
I had to do something similar to get UPN claims to come over, in circumstances similar to yours. I'm not sure whether it will matter that the LDAP attribute is potentially plural.
My application has some features that are accessible to all users, and some other features to which access should be restricted to authenticated users only. All these restricted features exists within some set of GWT Places, thus, all Places available in application can be divided into two groups: "accessible for all", and "restricted". In my opinion, places with restricted access, could implement some interface (let's say it would be RestrictedAccess), and if user proceeds to one of them, and it has not been authenticated yet, it will be redirected to the login screen - it's more OO-approach than applying filters basis on URL.
What I'm trying to achieve is:
Information about if user has been
authenticated or not should be
stored on server (it's not something
that could be stored in a cookie...)
Login page is a standard GWT place+view+activity (!)
User name & password validation is done on the server side.
So far, I've introduced RestrictedAccess interface, which is implemented by some set of places. My FilteredActivityMapper.Filter implementation, which is passed to the FilteredActivityMapper wrapping application activity mapper has the following logic:
Place filter(Place place) {
if (place instanceof RestrictedAccess && !userHasBeenAuthenticated()) {
return new LoginPlace();
}
// return the original place - user has been already authenticated or
// place is accesible for all users
return place;
}
private boolean userHasBeenAuthenticated() {
// remote call - how to do ???
}
The problem is with userHasBeenAuthenticated() method (user should not be redirected to the LoginPlace, if it has been already authenticated). If I want to store this information on the server-side, I have to do GWT RPC/request factory call here, but both are asynchronous, so I cannot work on its result in the filter method.
I know that I can use web.xml filters or some external framework (e.g. spring security), but none of this approach allows me to have login page as a standard GWT - based form, or indicating in the more OO way that access to some place should be restricted.
Thanks in advance for any hints
EDIT: I've started to wondering if places filtering (restricted/not restricted) should take place on the client side at all. If, as it was suggested, there is a possibility to hack code indicating if user has been authenticated or not, there is also possibility to hack places filtering code, so that it will be possible to access restricted places without signing in.
Piotrek,
I think there is a security issue with calling userHasBeenAuthenticated() - it would be possible to hack the client side code to return true every time this function is called.
The solution I've implemented is to simply return SC_UNAUTHORIZED if an unauthenticated user attempts to access any remote service. I've overridden the RequestFactory onResponseReceived function which redirects to a login page if the response is SC_UNAUTHORIZED. Idea taken from:
http://code.google.com/p/google-web-toolkit/source/browse/trunk/samples/expenses/src/main/java/com/google/gwt/sample/gaerequest/client/GaeAuthRequestTransport.java
This works for our situation where the Activities and Places are all data-centric - each place change retrieves data from the server. If a user isn't authenticated they simply don't get the data and get redirected to a login page.
I realize your situation is slightly different in that some places are accessible to everyone, in which case you could configure only the restricted services to return SC_UNAUTHORIZED.
I have a similar application with the same requirements. As yet I have not got round to to the implementation but I was thinking along the same lines.
What I was planning on doing is storing the authentication state client side in an AuthenticationManager class. When the app starts I was going to request the login info from the server (I was thinking of running on app engine so I would get the authentication state and also get the open id login/logout URLs) and store this in the AuthenticationManager. Acegi/Spring Security works in a simlar way so this info is available server side if you use those too.
When the user logs in/out they will be redirected by the server and the new state will be retrieved. This should keep the client authentication state in line with the server. Each RPC request on the server has to be checked for authentication too. I was using the gwt-dispacth library and this has some rudimentary authentication checking and cross site script protection in in too (although I think latest GWT has this for generic RPC).
One issue is session timeouts. Again the gwt-dispath library has some code that detects this and returns session expired exceptions to the client which can be intercepted and the auth manager updated.
Hope that makes some sense.