Dynamic OpenIdConnectOptions for multi-tenancy in Asp.net Core 2.1-* - azure-active-directory

I am working with aspnetcore v2.1 (latest dev branches) in order to create a multi-tenant app where each tenant authenticates against their own Azure B2C AD tenant. This aproach was chosen so that email/password selections and social login associations are unique per-tenant.
Instead of a static ClientId applied in Startup.ConfigureServices, I want to apply the correct ClientId and Authority based on the current tenant identity (which I determine based on the hostname). Based on previous inspection of the 2.0-* code, I had been using an IOptionsSnapshot to allow me to apply the correct options as shown below.
In Startup.ConfigureServices:
services.AddSingleton<IOptionsSnapshot<OpenIdConnectOptions>, OpenIdConnectOptionsSnapshot>();
services.AddAuthentication().AddCookie().AddOpenIdConnect();
In Startup.Configure:
app.UseAuthentication();
With an implementation of :
public class OpenIdConnectOptionsSnapshot : IOptionsSnapshot<OpenIdConnectOptions>
However, now I find that my OpenIdConnectOptionsSnapshot is no longer being instantiated or referenced.
What is the correct way to apply a dynamic per-tenant ClientId, Authority, etc under AspNetCore Security 2.1.0-*?
(I am open to "you're doing it completely wrong" and suggestions of different ways to achieve multi-tenancy for tenants that have no pre-existing AzureAD footprint)

Try using IOptionsMonitor instead, we changed how IOptionsSnapshot worked fairly late in 2.0 and switched auth over to use the monitor instead.
OptionsSnapshot is now scoped

Related

Azure Active Directory override claim value in OIDC id-token

I'm looking for a way to ensure that the unique_name claim value is always the user email for a specific app in AAD which is accessed via SSO. I've noticed that for users which have an AAD account, the unique_name claim value is the email address, but for external users (invited to the directory) it is prefixed by "live.com#" (e.g. live.com#email#domain.com). The application uses the OIDC protocol. I've been trying different things from Microsoft docs:
Setting a different source for the unique_name claim
https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-saml-claims-customization
(I'm not sure if this applies, given the app uses OIDC, not SAML)
(By the way, that documentation is a bit out of date because I found that the "User Attributes & Claims" section is now in SSO https://learn.microsoft.com/en-us/answers/questions/248748/cant39-find-the-34user-attributes-amp-claims34-sec.html)
Building a claim-mapping policy
https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-claims-mapping
None of these worked.
I know this would be trivial in AAD B2C with custom policies. Is it possible to override the unique_name claim with the email claim in AAD?
Update 07/01/2022
After further investigation I found that the unique_name claim is only supported in v1 tokens https://learn.microsoft.com/en-us/azure/active-directory/develop/id-tokens, https://github.com/MicrosoftDocs/azure-docs/issues/62971 (the current version is v2) and it's transformed into "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" by the ADAL client. This customization I'm after is for a legacy .NET app which uses ADAL on the v1 AAD endpoint.
The only way I can think of how to achieve this is via a claim-mapping policy, however I haven't been able to do make it work. Any ideas would be appreciated.
I believe I've found an answer to my question and the conclusion is that it's not possible to override the unique_name claim, because it's a restricted claim. The closest I got was to map the mail claim to a custom claim (e.g. unique_name2). This is what I did:
https://learn.microsoft.com/en-gb/azure/active-directory/develop/active-directory-claims-mapping
I installed the Azure AD PowerShell Module public preview (I had to uninstall the AD Powershell module first because I already had a later version)
I followed the instructions on the document above to connect to AAD and view any existing claim mapping policies
Connect-AzureAD -Confirm
Get-AzureADPolicy
I followed the instructions on https://learn.microsoft.com/en-us/answers/questions/80295/claim-transformation-in-azure-id-token-upn-data-to.html to build a claim mapping policy, although I had to adapt it because it wasn't working. I ran the following commands:
3.1 I used the search command to search for my app (called "test").
GET-AzureADServicePrincipal -SearchString Test
This outputs a table like the following:
ObjectId AppId DisplayName
3.2 I ran the following command to create a new mapping policy which maps the "mail" claim to the "unique_name2" claim. (When I tried mapping to "unique_name" it works, but the "unique_name" claim doesn't get overridden, because it's a restricted claim)
$Policy=New-AzureADPolicy -Definition
#('{"ClaimsMappingPolicy":{"Version":1,"IncludeBasicClaimSet":"true",
"ClaimsSchema":[{"Source":"user","ID":"mail"},{"Source":"transformation","ID":"DataJoin","TransformationId":"JoinTheData","JwtClaimType":"unique_name2"}],"ClaimsTransformations":[{"ID":"JoinTheData","TransformationMethod":"Join","InputClaims":[{"ClaimTypeReferenceId":"mail","TransformationClaimType":"string1"}],
"InputParameters":
[{"ID":"string2","Value":""},{"ID":"separator","Value":""}],"OutputClaims":[{"ClaimTypeReferenceId":"DataJoin","TransformationClaimType":"outputClaim"}]}]}}')
-DisplayName "MapMailToUniqueName2" -Type "ClaimsMappingPolicy"
3.3 I ran the following command to add the custom policy to my app service principal (copying the id from step 3.1). This means the policy will be applied only to that specific app.
Add-AzureADServicePrincipalPolicy -Id {my app service principal id}
-RefObjectId $Policy.Id
That's all. When I tested SSO on my app, I had an additional claim "unique_name2" which contains the value of the "mail" claim.
There probably is a more efficient way of creating the mapping policy in step 3.2 but the way described in the step 3 link wasn't working for me so I came up with a concatenation to a blank string.
It doesn't seem possible to override the unique_naim claim because it's a restricted claim. I later found that this is actually documented here https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/active-directory/develop/reference-claims-mapping-policy-type.md. I wish I had found this earlier.

Identityserver and SQL server Database

we are looking for brand new implementation for Identityserver4,
I wnet thru the documentation and install the Project on VS2017 and DB in sqlserver.
Database is created with the default migration script provided for bot ConfigurationData as well as Operational DAta.
I am very much confused , where the user will be how the clients will be add etc?
Also in the startup the default ASPNEtIdentity is add, but in database there is no ApplicationUser table, so where the userdata will be?
My requirement is simple - User will be aple to login to application by his credentials(or may be by 3rd party application) and will use the application or
API will directly use Identity server to with clientcredential scope,
Here please do let me know:
Should I introduce AspNetIdentity or Not, and Why?
Where is the user table and Password of the user in the database generated by the migration.
How we can add User clients and resources to the Created Database
Do I need to add Login/Logout page ?
In API APIResource is used to defined the Resources "api1" and same is used by the client code to get the access but this "api1" is not used anywhere with the definition/signature of the Method, so ow will it be correlated?
First off, IdentityServer4 on it's own does not handle users or authentication thereof - you either need to use ASP.Net Identity and the integration library that hooks it up to IdentityServer4 or build all of that stuff yourself.
So to sum up:
Yes you'll need to use that or roll your own user store and authentication logic
Either provided by ASP.Net Identity or built yourself
https://www.identityserver.com/documentation/admin-ui/ may fit your needs or you could build your own tooling and/or scripts to do so
Yes although the quickstart samples provide good basic starting points
The bearer token middleware by default will validate the audience (ApiResource.Name) but I prefer to enforce scope at a more granular controller or action level using a filter (e.g. [ScopeAuthorize("my.api")] public MyResult ApiAction() { ... }). This filter will check that the specified scope is defined in the set of claims in the ClaimsPrincipal).

How to support multiple login scenarios in multi-tenanted Azure Active Directory (AAD)

Our application (referred to as "XYZ_App" below) is a multi-tenant SaaS application. We are in the process of making it available for Microsoft AppSource as a multi-tenanted "Web app / API" (referred to as "AppSourceXYZ_App" below).
We started our OpenID Connect implementation with endpoints pointing to “common” as per stated in the documentation when multi-tenancy is desired/required.
In XYZ_App, we added information in the system to know what AAD instance each XYZ_App tenant is associated with (using the GUID Microsoft assigned to this AAD instance, we are NOT using the "rename-safe.onmicrosoft.com" representation).
When using the “common” endpoints, we had to manually validate the issuer from the JWT to make sure it was the expected one: a user could access XYZ_App requesting access to XYZ_App’s tenant associated with contoso.onmicrosoft.com, get directed to “login.microsoftonline.com/common” to authenticate and then decide to authenticate with a user from another AAD instance (referred to as "anotherAADInstance.onmicrosoft.com" below). In this scenario, even though a user could successfully authenticate on anotherAADInstance.onmicrosoft.com, XYZ_App’s redirect URI must make sure the JWT issuer is the one from contoso.onmicrosoft.com. I’ll refer to this setup as Scenario_1.
With that scenario in mind, we thought about NOT using “common” and customize the requests going to login.microsoftonline.com on the fly; attempting to “jail” requests to be forced to authenticate against a specific AAD instance. We would still need to perform our validation in the redirect URI to make sure the issuer is the appropriate one, but we thought this approach might make our lives easier. I’ll refer to this setup as Scenario_2.
Do you envision Scenario_2 is viable in the long run or is it too short-sighted ? Based on my current knowledge of OpenID Connect, one limitation I can see with Scenario_2 is that it would become problematic to support “broker accounts” into our app.
Explanation of “broker accounts”: in our industry, some external users are allowed access to the system. Let’s say I have a company called “BrokerCo” (with their own brokerco.onmicrosoft.com AAD instance) who has 2 employees: Broker1 and Broker2. BOTH anotherAADInstance and contoso hired Broker1 and Broker2 to get broker services to perform tasks in XYZ_App; requiring XYZApp to grant them access. What is the ideal way for authentication from an OpenID Connect standpoint ? If XYZ_App were to use “login.microsoftonline.com/common” for authentication (like in Scenario_1; as opposed to “jailed” access like in Scenario_2), Broker1 and Broker2 could authenticate with brokerco.onmicrosoft.com (no AAD "External users" for anotherAADInstance nor contoso), but they would then get to redirect URI with an issuer that is different than what XYZ_App’s anotherAADInstance and contoso tenants are configured for... I feel like I’m back to square 1...
Do you have suggestions or pointers to solve this issue ?
Background context:
While playing with OpenID Connect issuers, I got the following error message:
AADSTS50020: User account 'testuser#anotherAADInstance.onmicrosoft.com' from identity provider 'https://sts.windows.net/XXXXXXXX-fake-GUID-9bZZ-XXXXxxxxXXXX/' does not exist in tenant 'XYZ Publisher' and cannot access the application 'YYYYYYYY-fake0-GUID-YYYYyyyyYYYY' in that tenant. The account needs to be added as an external user in the tenant first. Sign out and sign in again with a different Azure Active Directory user account.
Thanks in advance !
Your question has multiple layers, trying to address most of them:
AppSource is about trial experiences for new users: this mean that any corporate account around the globe can potentially be an user of your SaaS application - or at least to the trial experience of your application, therefore the first thing you need to think when integrating with AppSource is how easy it has to be for a potential user to experience your app for the first time.
With that in mind, AppSource recommends that the trial of application is build on such a way that allows any user from any organization to sign-in, and therefore a multi-tenant approach for your application is the recommended setup for any application.
The single-tenant approach requires more control on your side, and for a trial experience - it means that the user cannot try your application right away because the operation you have to do on your single-tenant application is to add the user to an Azure Active Directory tenant as a guest user. This guest account will need then to wait receiving an email to accept the invitation to join to this tenant you are adding the user to then sign-in to your application.
Therefore your scenario 1 is the best scenario thinking on a trial experience in general, and also in general require less management (as you'd not need to create/ manage each individual account that needs to access your application as guest users of your Azure AD instance).
However some concerns you listed - that this scenario bringing are valid: Because you are accepting the common endpoint, you are saying basically that any user can sign-in to any tenant to your application, and this may not be desirable. In addition, the scenario you listed that a user can generate a token for any application is also valid, however, you can add additional checks to make this more secure and that any token generated by another authentication is blocked:
You can validate the 'audience' claim to guarantee that the token was issued to your application
You can eventually check the 'tid'/'iss' claims against of a list of tenant Ids in your database to see if that the user's organization is a valid organization in your application -- this would be valid for non-trial users/ organizations.
More information in this article.
About scenario '2' and broker accounts:
This scenario could be interpreted in two different ways:
Broker accounts are guest accounts of a customers' Azure AD tenant
Broker accounts are third party accounts but are not actually added as a user of anotherAADInstance or contoso AD
If your case is '1' then you're right: if your application needs to authenticate guest users that belong to another Azure AD tenant, then common endpoint could not be used directly.
If your case is '2' then what you'd need to do is to continue using the common endpoint and somewhat after the user is authenticated ask them to choose the company. I am describing this on generic terms without fully understanding this scenario. Perhaps this is not simple as you want the remote company to control this and not the user - so some additional complexities may need to be handled here.
A note is that if your scenario is scenario 1. - in theory - you can still use an hybrid approach, where you'd ask user to type the username inside the application and the company that they want to sign-in, then check if you need to validate the user against common or tenant-id endpoint, preparing the request and then sending a login_hint argument when authenticating. More information here

Moving Azure AD B2C custom user attributes across new environments

I've created a 'customerId' user attributes in my development AD B2C tenant. When querying these via the graph API I now get the following;
"extension_b0ba955412524ac8be63e24fa7eb0c23_customerId": "2"
Perfect! I use JSON.Net to convert this to a strongly typed object, and hence my POCO has the following property.
public string extension_b0ba955412524ac8be63e24fa7eb0c23_customerId { get; set; }
Not great, but I can live with it. My concern is that when I create these properties in our staging and production environments, the attribute names will change and I will have to rewrite a lot of code. How do I migrate these user attributes to our other environments, ensuring the property names do not change?
Custom user attributes in Azure AD B2C will always have a guid in the name that's unique per Azure AD B2C tenant.
This guid is the Application ID of the 'b2c-extensions-app' and will be the same for all custom attributes within a tenant, but different across tenants.
Given this behavior, you could make the code that handles custom user attributes dynamic such that it derives the full name* using the following pattern:
extension_<appId>_<attributeName>
And obtain the appID via the Graph:
https://graph.windows.net/yourtenant.onmicrosoft.com/applications?$filter=displayName eq 'b2c-extensions-app'
Of course, you can always provide feedback requesting that this is made easier in in the Azure AD B2C feedback forum.

IdentityServer3 - which for ActiveDirectory: MembershipReboot / AspNetIdentity / UserService

I hope the following question makes sense:
I manage my users in ActiveDirectory.
I authenticate them via IdentityServer3.
I authorize the APIs via the AD groups that the user is in (acting as security roles).
How should I set up IdentityServer3:
Must I use my own custom UserService to access ActiveDirectory?
and does that replace the MembershipReboot / AspNetIdentity support (or am I misunderstanding what the UserService is)?
Or should I use one of the MembershipReboot / AspNetIdentity packages from IdentityServer3, and somehow customize them to map to ActiveDirectory (and if so, how)?
Seems there is no "mapping" and should not be a mapping from AD to a membership-reboot or an aspidentity or the newer identity-reboot user stores. The reason seems to be simple: mr and aspid or ir are all ways to store the user information in a persistent way (some sort of database or repository), which is already done in AD.
The userservice is enough. It causes the ASP Identity objects to be populated, and the middleware to work as expected, calling user authentication, and user or resource authorization correctly and automatically, after the client calls are "decorated" with "Authorize" attributes or after returning from the OP (the OpenID-Connect Provider) or from separate authorization or resource providers, in security calls.
Answer update: Now in IdentityServer4 the UserService has been deprecated and instead you use IResourceOwnerPasswordValidator.
See here for working code and a detailed explanation, in the answer after the accepted one (vote it up please)
IdentityServer4 register UserService and get users from database in asp.net core

Resources