Add Roles fetched from SQL Server as Claims to AD FS Relying Party Trust - asp.net

I'm authenticating users of an asp.net mvc web site by using ADFS Server 2016 passive redirection, and I cannot get claims from a SQL attribute store. I'm interested in discovering what I am doing wrong or missing.
Side note: I'm using the System.Identity libraries from Framework 4.5 (I'm not referencing the Microsoft.Identity libraries created for older framework versions; most ADFS code samples that I stumble across use these old libraries).
The basics are working well. All of this is in one domain. I have my asp.net web.config set up to redirect users to my ADFS server for authentication. The ADFS server successfully authenticates and redirects users back to my asp.net web site. On the ADFS I have one Claim Issuance Policy rule where I simply pass back all claims from the Active Directory.
On the web site I am able to iterate through the user's Claims collection and display them. Here is the code from the *.cshtml page where I iterate though the claims, it works fine:
#using System.Security.Claims;
#{
var currentPrincipalIdentity = (ClaimsIdentity)System.Threading.Thread.CurrentPrincipal.Identity;
}
#foreach (Claim claim in currentPrincipalIdentity.Claims)
{
<br/>#claim.Type : #claim.Value
}
In addition to these claims from Active Directory, I want to fetch a bunch of roles from a SQL Server database and add them to the Claims collection as roles. I'm fetching the roles from a legacy asp.net Membership database. As step 1 I just want to hard-code the username in the SQL statement (eventually I will need to figure out how to pass the username as a parameter to the SQL statement, but that will be step 2).
First, I gave the identity that the ADFS server runs under read/write/execute permissions on my SQL Server (when I take these permissions away I get a permissions error, which gives me confidence that my SQL statement is executing).
In my AD FS I added a SQL Server Attribute Store by right-clicking the "Attribute Stores" node, selecting an Attribute store type of "SQL", named is "SQLServer", and added a connection string like so:
Server=SqlDev01; Database=MyLegacyMembershipDatabase; Integrated Security=SSPI;
I then select the "Relying Party Trusts" folder, select the trust I am interested in, and select "Edit Claim Issuance Policies." I have one rule there that works; it simply passes back all Active Directory claims. I can see all of these claims on my web page (upn, name, windowsaccountname, all of my group sids, and etc):
c:[]
=> issue(claim = c);
I'm trying to add a 2nd custom rule to read a legacy membership database. In my ADFS I click "Add Rule", "Send Claims Using a Custom Rule", and add this as the rule:
c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"]
=> add(store = "SQLServer", types =
("http://schemas.microsoft.com/ws/2008/06/identity/claims/role"), query =
"select r.RoleName AS Role from dbo.aspnet_Roles r INNER JOIN
dbo.aspnet_UsersInRoles uir ON r.RoleId = uir.RoleId INNER JOIN
dbo.aspnet_Users u ON uir.UserId = u.UserId WHERE u.UserName = '[hard-coded
value here]' OR u.UserName={0}", param = c.Value);
It saves fine, but when I re-run the page nothing changes; I still get the original collection of Active Directory claims, but not the data from SQL Server.
I am confident the SQL Server statement is executing, because if I remove permissions for the identity that ADFS runs under from the SQL Server I get an error, and if I deliberately garble the SQL syntax I get an error. If I reverse these deliberate mistakes then the page functions properly again. But I never see the Roles that I want to see in the Claims collection.
From my understanding of custom rules, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" is passed as a parameter into the query, that is why I have the OR statement above; my ultimate goal is to pass the user's UPN as a parameter into the SQL query.
Am I missing something or doing something wrong? Bonus question--assuming I get this working, can you tell me how to pass the user's UPN as a parameter into the SQL query?

Try an "issue" rule rather than an "add".

Related

cosmos db, generate authentication key on client for Azure Table endpoint

Cosmos DB, API Azure Tables, gives you 2 endpoints in the Overview blade
Document Endpoint
Azure Table Endpoint
An example of (1) is
https://myname.documents.azure.com/dbs/tempdb/colls
An example of (2) is
https://myname.table.cosmosdb.azure.com/FirstTestTable?$filter=PartitionKey%20eq%20'car'%20and%20RowKey%20eq%20'124'
You can create the authorization code for (1) on the client using the prerequest code from this Postman script: https://github.com/MicrosoftCSA/documentdb-postman-collection/blob/master/DocumentDB.postman_collection.json
Which will give you a code like this:
Authorization: type%3Dmaster%26ver%3D1.0%26sig%3DavFQkBscU...
This is useful for playing with the rest urls
For (2) the only code I could find to generate a code that works was on the server side and gives you a code like this:
Authorization: SharedKey myname:JXkSGZlcB1gX8Mjuu...
I had to get this out of Fiddler
My questions
(i) Can you generate a code for case (2) above on the client like you can for case (1)
(ii) Can you securely use Cosmos DB from the client?
If you go to the Azure Portal for a GA Table API account you won't see the document endpoint anymore. Instead only the Azure Table Endpoint is advertised (e.g. X.table.cosmosdb.azure.com). So we'll focus on that.
When using anything but direct mode with the .NET SDK, our existing SDKs when talking to X.table.cosmosdb.azure.com endpoint are using the SharedKey authentication scheme. There is also a SharedKeyLight scheme which should also work. Both are documented in https://learn.microsoft.com/en-us/rest/api/storageservices/authentication-for-the-azure-storage-services. Make sure you read the sections specifically on the Table Service. The thing to notice is that a SharedKey header is directly tied to the request it is associated with. So basically every request needs a unique header. This is useful for security because it means that a leaked header can only be used for a limited time to replay a specific request. It can't be used to authorize other requests. But of course that is exactly what you are trying to do.
An alternative is the SharedKeyLight header which is a bit easier to implement as it just requires a date and the a URL.
But we don't have externalized code libraries to really help with either.
But there is another solution that is much friendly to things like Fiddler or Postman, which is to use a SAS URL as defined in https://blogs.msdn.microsoft.com/windowsazurestorage/2012/06/12/introducing-table-sas-shared-access-signature-queue-sas-and-update-to-blob-sas/.
There are at least two ways to get a SAS token. One way is to generate one yourself. Here is some sample code to do that:
var connectionString = "DefaultEndpointsProtocol=https;AccountName=tableaccount;AccountKey=X;TableEndpoint=https://tableaccount.table.cosmosdb.azure.com:443/;";
var tableName = "ATable";
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
CloudTable table = tableClient.GetTableReference(tableName);
await table.CreateIfNotExistsAsync();
SharedAccessTablePolicy policy = new SharedAccessTablePolicy()
{
SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(1000),
Permissions = SharedAccessTablePermissions.Add
| SharedAccessTablePermissions.Query
| SharedAccessTablePermissions.Update
| SharedAccessTablePermissions.Delete
};
string sasToken = table.GetSharedAccessSignature(
policy, null, null, null, null, null);
This returns the query portion of the URL you will need to create a SAS URL.
Another, code free way, to get a SAS URL is to go to https://azure.microsoft.com/en-us/features/storage-explorer/ and download the Azure Storage Explorer. When you start it up it will show you the "Connect to Azure Storage" dialog. In that case:
Select "Use a connection string or a shared access signature URI" and click next
Select "Use a connection string" and paste in your connection string from the Azure Portal for your Azure Cosmos DB Table API account and click Next and then click Connect in the next dialog
In the Explorer pane on the left look for your account under "Storage Accounts" (NOT Cosmos DB Accounts (Preview)) and then click on Tables and then right click on the specific table you want to explore. In the right click dialog you will see an entry for "Get Shared Access Signature", click on that.
A new dialog titled "Generate Shared Access Signature" will show up. Unfortunately so will an error dialog complaining about "NotImplemented", you can ignore that. Just click OK on the error dialog.
Now you can choose how to configure your SAS, I usually just take the defaults since that gives the widest access permission. Now click Create.
The result will be a dialog with both a complete URL and a query string.
So now we can take that URL (or create it ourselves using the query output from the code) and create a fiddler request:
GET https://tableaccount.table.cosmosdb.azure.com/ATable?se=2018-01-12T05%3A22%3A00Z&sp=raud&sv=2017-04-17&tn=atable&sig=X&$filter=PartitionKey%20eq%20'Foo'%20and%20RowKey%20eq%20'bar' HTTP/1.1
User-Agent: Fiddler
Host: tableaccount.table.cosmosdb.azure.com
Accept: application/json;odata=nometadata
DataServiceVersion: 3.0
To make the request more interesting I added a $filter operation. This is an OData filter that lets us explore the content. Note, btw, to make filter work both the Accept and DataServiceVersion headers are needed. But you can use the base URL (e.g. without the filter parameter) to make any of the REST API calls on a specific table.
Do be aware that the SAS token is scoped to an individual table. So higher level operations won't work with this SAS token.

ASP Identity : Binding Sessions To Request Url (Sub Domains)

I don't think the title of the question is particularly accurate but that's how best i could title it.
Without summarizing, I have an MVC app hosted on Microsoft Azure. The app was built for multiple institutions (each connecting to a separate database) but the Login Module (Asp Identity) is in a central database (users are identified by their institution code). So during deployment, a sub domain is created (still pointing to the app on azure).
My problem is, the app has no regard for the Request Url, the sessions are maintained across domains. This is a serious problem because i cache User data (by session). So if a user Logs in on "domain1.myapp.com" , then opens another tab , logs into "domain2.myapp.com" , all data cached for the user logged in for "domain1" will be used for the user logged in at "domain2". The app doesn't bother to get data for the user in "domain2" since the key for that data value is already present in the session cache.
Okay, I hope the problem is understood. How do i get past this.
Ideas ? implementation ?
EDIT 1
I insert data into the cache by,
HttpRuntime.Cache.Insert("KEY", "VALUE", null, DateTime.Now.AddMinutes(30),Cache.NoSlidingExpiration);
Your caching strategy needs to change when you cache per user and per domain. The simplest approach is just to add the domain and user name to the cache key, which will keep each cache in a separate per user and per domain bucket. Make sure you put a delimiter between the values to ensure uniqueness of the key.
var domain = HttpContext.Request.Url.DnsSafeHost;
var user = HttpContext.User.Identity.Name;
var key = "__" + domain + "_" + user + "_" + "KEY";
HttpRuntime.Cache.Insert(key, "VALUE", null, DateTime.Now.AddMinutes(30),Cache.NoSlidingExpiration);
Note that if you use HttpContext.Session, it will automatically put different domain information into separate buckets because it is based on a cookie (which by default is domain specific). However, HttpContext.Session comes with its own set of problems.

The remote server returned an error: (401) Unauthorized. Using CSOM in ASP.NET

I'm tried to pull some SharePoint 2013 list data I created which works fine when running locally on my machine and when run locally one the server. I'm user the same credentials when running both locally and locally on the server. The issue is when I publish and navigate to my ASP.NET app on the server I get the "The remote server returned an error: (401) Unauthorized." Error...
I've looked at a bunch of the posts on stackoverflow and some other articles on the web
This points out that the context seems to be using IUSR:
http://blogs.msdn.com/b/sridhara/archive/2014/02/06/sharepoint-2013-csom-call-from-web-part-fails-with-401-for-all-users.aspx
This one mentions to try setting the default network credentials:
https://sharepoint.stackexchange.com/questions/10364/http-401-unauthorized-using-the-managed-client-object-model
I've tried using the fixes mentioned in the article as well as trying to force the context to use DefaultNetworkCredentials but no luck. I would like for the app to use the credentials of the logged in user and not the machine...
Here is the code I'm using:
SP.ClientContext context = new SP.ClientContext("MySPDevInstance");
context.Credentials = CredentialCache.DefaultNetworkCredentials;
Entity entity = context.Web.GetEntity(collectionNamespace, collectionName);
LobSystem lobSystem = entity.GetLobSystem();
LobSystemInstanceCollection lobSystemInstanceCollection = lobSystem.GetLobSystemInstances();
context.Load(lobSystemInstanceCollection);
context.ExecuteQuery();
LobSystemInstance lobSystemInstance = lobSystemInstanceCollection[0];
FilterCollection filterCollection = entity.GetFilters(filter);
filterCollection.SetFilterValue("LimitFilter", 0, 1000);
EntityInstanceCollection items = entity.FindFiltered(filterCollection, filter, lobSystemInstance);
The server is running IIS 6.0
Any advice would be much appreciated!
Thank you
I presume your ASP.NET web site is using Windows Integrated (NTLM) authentication. A user authenticated this way cannot authenticate to a second location from the server side (the web server.) You are experiencing what is known as the "double-hop" (1) limitation of NTLM. You must use a dedicated account on the server side, or if you really do want to use the logged-in user's identity, you must use an authentication scheme that permits delegation, such as Kerberos.
If you really need the user's identity to access SharePoint data and you cannot change the authentication scheme, then the best way to do this is to use the JavaScript CSOM. This means the user is authenticating directly to the SharePoint server (a single hop, not double) and your ASP.NET site serves the page containing this script to the user.
(1) http://blogs.msdn.com/b/knowledgecast/archive/2007/01/31/the-double-hop-problem.aspx
Use Default Credentials worked for me:
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
httpWebRequest.UseDefaultCredentials = true;
Setup the crendentials by code:
SP.ClientContext context = new SP.ClientContext("MySPDevInstance");
context.Credentials = new NetworkCredential("username", "password");
You should put this at the configuration file to change it without publishing or recompiling the application.
Just to add one more setting that I encountered. If the account is restricted to access only certain servers than add the client machine to that account as well. For example if a web application is hosted on Server A and trying to connect to SharePoint 2010 on Server B with account ABC then make sure that account has access to Server A in Active Directory. Normally the AD account doesn't have restrictions to connect to machines but in my case the account was restricted to only certain machines. I added my web application hosted server to the account and it worked.

ADFS Skip Home Realm Discovery using integrated auth

My scenario:
I've got a .net web application where people log into the app with forms authentication, and my forms authentication code sends certain users off to ADFS to be verified. I have multiple federations, including my own Active Directory server for internal users. So, for users that need to be sent to ADFS for authentication, I do something like this:
WSFederationAuthenticationModule instance = FederatedAuthentication.WSFederationAuthenticationModule;
SignInRequestMessage request = instance.CreateSignInRequest(Guid.NewGuid().ToString(), instance.Realm, true);
request.AuthenticationType = "urn:federation:authentication:windows";
//maybe you just don't need to specify the homerealm for ad, because there can only be one AD (integrated) trust?
//request.HomeRealm = "I-no-idea-what-to-put-here-for-AD";
Response.Redirect(request.WriteQueryString());
When you do this, you set the .HomeRealm property which populates the whr attribute for you on the query string. This should make AD FS skip the HRD (Home Realm Discovery) page. For most of my federations (remote STSs) it's very clear what value I need to put in the property, you can pull the value from the dropdown box on the HRD page itself, or you can go into the AD FS management tool and pull the value from the trust properties. For the AD trust, however, there is no value in the dropdown list (empty string), and you can't go to properties in the management console. So the question is really this: if I want to preselect the AD trust through the whr parameter, what do I set the HomeRealm Property to?
P.S. you'll see the comment there: "maybe you just don't need to specify the homerealm for ad, because there can only be one AD (integrated) trust?" I think this might be the case, but I haven't proved it yet. I will answer this later if my guess is correct.
From the code in the original question:
request.AuthenticationType = "urn:federation:authentication:windows";
That particular line didn't appear to have any effect. Also, the hope that no HomeRealm need be specified was also in vain. It turns out that you must, in fact, specify the HomeRealm. After searching google for some time, I found many statements hinting that the Active Directory trust was the same as trusting the ADFS server itself. So as a wild shot in the dark I tried putting in the URI of the ADFS server, and, much to my pleasure and surprise, this worked.
So, to be more clear, if you go to the web.config of the relying party you should have something set up similar to this:
<issuerNameRegistry type="System.IdentityModel.Tokens.ValidatingIssuerNameRegistry, System.IdentityModel.Tokens.ValidatingIssuerNameRegistry">
<authority name="http://testadfs.test.com/adfs/services/trust">
<keys>
<add thumbprint="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" />
</keys>
<validIssuers>
<add name="http://testadfs.test.com/adfs/services/trust" />
</validIssuers>
</authority>
</issuerNameRegistry>
I ended up setting the value of request.HomeRealm to the value that you see there in the validIssuers and authority elements. This gave me the desired effect of causing ADFS to automatically select the Active Directory claims provider trust.
I should be clear that my ADFS server is NOT the domain controller, and yet this still works.

Check other user's role membership (IsInRole, WindowsIdentity/Principal)

I'm writing ASP.NET code to run on an internal network where Windows Authentication will be used. Certain operations will require me to run a group membership check on other users (not the current user)
NOTE: I am NOT trying to impersonate this account, or access any information in the context of this other user. Just trying to find out what kind of user they are for internal business logic.
My first thought was to use
new WindowsPrincipal(new WindowsIdentity("MACHINENAME\\username"))
.IsInRole("MACHINENAME\\Group1")
However, the WindowsIdentity constructor fails with a SecurityException "The name provided is not a properly formed account name".
If I strip MACHINENAME\ from the parameter, I get a different error: There are currently no logon servers available to service the logon request
The WindowsTokenRoleProvider role provider explicitly only works with the current user, and will not check other user accounts.
Are there security restrictions to checking roles of other users? Would it make a difference if the web server was on a domain and I were checking domain accounts?
In the end, I'll need to have this work on an AD domain, but would prefer a solution that will work on either local or AD accounts.
Thank you
UPDATE: I've been able to test this on a domain now -- the code does work in an AD context so long as I don't use the domain name (test "username" against "Group1", not "DOMAIN\username" against "DOMAIN\Group1")
So how would I get this to work in the context of local users and groups?
Based on Rob A's comment, PrincipalContext and UserPrincipal are the classes I apparently need to use:
using (PrincipalContext ctx = new PrincipalContext(ContextType.Machine))
{
var u = UserPrincipal.FindByIdentity(ctx, IdentityType.Name, "username");
var b = u.IsMemberOf(ctx, IdentityType.Name, "Group1");
var groups = u.GetAuthorizationGroups();
}
And by altering the ContextType, can switch between local accounts and AD accounts. I wish this was built into a RoleProvider, but I guess that's something I'd have to do for myself.

Resources