First project working with both single sign on and Azure, so maybe I'm just not doing it correctly. Before using SSO, I would generate the token myself. This allowed me to put whatever I wanted inside of the token, which I would use to determine permissions. Using react-adal with Azure AD SSO, the token is generated client side and all I get is the user identity. Following what I did in the past, I wrote a custom attribute to ensure the user has the appropriate permissions for the API call. But rather than just pulling the information from the token that is passed with every request, I have to query the permissions every time they make a request, essentially doubling the database hits.
Is there a way for me to handle roles/permissions (admin, manager, user, read-only, etc) using single sign on and application driven permissions without querying the database every time I need to check permissions?
Previous process:
User visits site > enters credentials > server authenticates, gets permissions, generates token with permissions, returns it to client > client passes token on every request > server validates and parses token > attribute checks parsed token to ensure user has necessary permission > completes request
Current process: User visits site > client authenticates with Azure AD and gets token > client passes token on every request > server gets authentication information from token > server queries database to get users permissions > attribute checks query results to ensure user has necessary permission > completes request
How can I make the current process better? Every google result I've found has only covered authentication and hasn't gone deep enough into the actual application for me to find a "best practice" or even any practice. Am I just doing it wrong?
Please take a look at the Application Roles related functionality with Azure AD to implement your custom RBAC. It should provide a good starting point for what you've mentioned, as roles collection will be available for you as part of incoming tokens from Azure AD.
Application Roles
Microsoft Documentation - Application Roles
Purpose - These roles are defined in the Application Manifest for an application that your organization is developing and that is registered in your Azure Active Directory. These roles are very specific to your application and can be used in application's code to implement Authorization logic for the authenticated users.
Sample Application (that uses this concept and does what you're looking for) -
Authorization in a web app using Azure AD application roles & role claims
Quick Explanation
1) Once you register your application with Azure AD, you can define custom roles (specific to your application) by editing the application manifest (JSON) in Azure AD.
Here's a sample JSON of what application role definition would look like:
"appRoles":
[
{
"allowedMemberTypes": [
"User"
],
"description": "Creators can create Surveys",
"displayName": "SurveyCreator",
"id": "1b4f816e-5eaf-48b9-8613-7923830595ad",
"isEnabled": true,
"value": "SurveyCreator"
},
{
"allowedMemberTypes": [
"User"
],
"description": "Administrators can manage the Surveys in their tenant",
"displayName": "SurveyAdmin",
"id": "c20e145e-5459-4a6c-a074-b942bbd4cfe1",
"isEnabled": true,
"value": "SurveyAdmin"
}
]
2) You will be able to assign these roles to Users/Groups/applications through Azure Portal or programmatically. (you could control the allowed member types for roles)
3) Now when the end users sign in to your application, the incoming Azure AD token will provide you a collection of role claims (based on whatever roles are assigned to the user) and you can take authorization decisions in your application.
if (context.User.HasClaim(ClaimTypes.Role, "Admin")) { ... }
Here is another related documentation on Microsoft Docs - Role-based and resource-based authorization
I see you have also tagged asp.net-core in your question. So if you're working with ASP.NET core application, you could possibly make use of policies as shown in Role-based authorization section of the link above.
public class SurveyCreatorRequirement : AuthorizationHandler<SurveyCreatorRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SurveyCreatorRequirement requirement)
{
if (context.User.HasClaim(ClaimTypes.Role, Roles.SurveyAdmin) ||
context.User.HasClaim(ClaimTypes.Role, Roles.SurveyCreator))
{
context.Succeed(requirement);
}
return Task.FromResult(0);
}
}
On a side note, I've seen cases where people chose to do some authorization logic based on which groups the users belonged to. This is just information and not something you need to do. I'm sharing some details for both, Roles and Groups in this answer, but definitely look at Application Roles first. You could even use a combination of both Roles and Groups for your Authorization strategy.
Groups
Groups can have multiple users or other groups as members. Again management of groups is possible through Azure Portal or programmatically.
NOTE: Groups are totally independent of your application, i.e. Azure AD groups can and do exist to serve a purpose of grouping members even without your application. Application Roles on the other hand are very specific to your application, they don't mean much to anyone except your application.
Sample app which makes decisions based on Groups
Authorization in a web app using Azure AD groups & group claims
Individual or Specific resource based authorization
Also know that when it gets to individual resource based authorization (and not a more generic role based authorization), then AFAIK you will need to go to your database or other persistent storage anyway because that's the only place which knows about a detailed mapping between permissions and individual objects in your system.
I have discussed about this in a related SO Post here - Resource based Authorization with Azure AD
Related
I have a web application that allows me to sign in to my OneDrive account using delegated permissions to authorize the app to browse my drive files on my behalf. I'm now trying to build a server-side job that needs to work with these files and therefore needs application permissions granted with admin consent.
I've followed various instructions to achieve this, but no matter what I do I keep getting this 403 error:
Either scp or roles claim need to be present in the token
The application I've registered in Azure portal has the application permission Files.Read.All and I've granted admin consent. I'm obtaining my access token as follows:
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create("<client-id>")
.WithClientSecret("<secret>")
.WithAuthority(new Uri("https://login.microsoftonline.com/common"))
var apiUrl = "https://graph.microsoft.com/";
string[] scopes = { $"{apiUrl}.default" };
result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
I'm then using the returned token to request "{apiUrl}v1.0/drives/<my-drive-id>/items/<drive-item-id>"
And this is where I get the access denied. Clearly the API expects my token to have either a roles claim or an scp claim.
After reading this related post on SO I did wonder if the problem is related to the fact I'm using the same app registration for the front end and back end operations (so it has a mix of delegated and application permissions) but I tried creating a new app registration with only the application permissions and it was the same error.
Also, that post suggests I should expect to see either and scp or a roles claim in my token (depending on choice of auth flow) but I get neither of these claims. That led me to this other SO post which suggests I need to explicitly include roles in my access token, but roles isn't listed as an optional claim in the Token configuration blade of the app registration.
So I'm stuck. Can anyone help?
UPDATE 1
I've tried constructing the auth request manually now using a POST to https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token and a form body that includes grant_type=client_credentials and now I'm finding the roles claim is returned in the token. So I guess the ConfidentialClientApplicationBuilder wasn't building a client credentials auth request as I had assumed. However, when I use this token to make the above Graph API request for a drive item I get the error:
Tenant does not have a SPO license
When I've encountered this before I've been told to use common instead of my tenant ID. However, when I make that change in this case I'm again left with a token that doesn't have a roles claim.
The absence of the roles claim indicates that you app (or service) hasn't been granted any application permissions (i.e. app roles) for the API (in this case, Microsoft Graph), in the tenant where the token request is being made.
You need to ensure the the application permissions (app roles) you expect the app to use to make the API call have been granted in the tenant where you are making the API call.
I am using identity server 4 for authentication and authorization, and user permissions are saved in JWT and then used on API-s to check if users has required permission.
But the problem is that JWT got too big and I would like to remove permissions from it, and make custom authorization on API-s so that its fetches permissions from identity server instead of getting it from JWT.
API would get only userId from JWT and then based on that fetch additional information from identity server. Is it possible to do something like that?
We basically have a similar problem in our application.
The way to solve this problem is using an event which is raised at the level of the API resource (the API which you are protecting by using JWT bearer tokens authentication) once the JWT token has been read from the incoming request and validated.
This event is called OnTokenValidated, see here for more details.
This is the top level plan:
keep your JWT bearer token minimal. At the very minimum it contains the subject id, which is the unique identifier of the user at the identity provider level. You can put other claims there, but the idea is that the JWT bearer token must be small
implement a way to get the user permissions given the user unique identifier (you can use the subject id as an identifier or any other id which makes sense in your system)
make the user permissions fetch mechanism of the previous point accessible via api call. Caching this API is a good idea, because usually permissions are stable. Defining a smart way to evict this cache is beyond the scope of this answer, but it's something you should definitely think about.
once you have fetched the user permissions (via an API call) you need to make them available to the ASP.NET core authorization framework. The simplest way to do so is create a custom claim type (for instance: "app_permission") and create one user claim per each user permission. Each of these permission claims has the custom claim type ("app_permission") and the permission name as the claim value. For instance a user having the two permissions "read-content" and "write-content" will have two claims both having "app_permission" as the claim type, the first one having "read-content" as the claim value and the second one having "write-content" as the claim value.
the permissions claims defined at the previous point can be injected in the user identity (at the API resource level) by defining an additional ClaimsIdentity for the user and by adding it to the current user identity. The process depicted here is quite similar to a claims transformation done by an MVC application using cookie authentication.
In the Startup class of your API resource, in the point where you register the authentication services, you can do something like this:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:8080";
options.Audience = "sample-api";
options.RequireHttpsMetadata = false;
// register callbacks for events
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
if (!context.Principal.Identity.IsAuthenticated)
{
return;
}
var subjectId = context.Principal.FindFirst(JwtClaimTypes.Subject)?.Value;
if (string.IsNullOrWhiteSpace(subjectId))
{
return;
}
// do whatever you want with the user subjectId in order to get user permissions.
//You can resolve services by using context.HttpContext.RequestServices which is an instance of IServiceProvider
//Usually you will perform an API call to fetch user permissions by using the subject id as the user unique identifier
// User permissions are usually transformed in additional user claims, so that they are accessible from ASP.NET core authorization handlers
var identity = new ClaimsIdentity(userPermissionsClaims);
context.Principal.AddIdentity(identity);
}
};
});
+1 for the accepted answer but I would just like to offer an alternative solution to this problem. If your permissions are pretty simple like readResource or writeResource then you could define all your permissions as enum and use integers instead of strings in JWT, that would reduce JWT size.
If permission list is still huge then you could also group permissions together so that the permission list is smaller for some customers e.g. merge readResource, writeResource, updateResource, deleteResource into one permission called crudResource.
My goal is to bypass the login screen and use Azure AD as the Identity Provider.
Given I am already logged in with my Azure AD user, I'd like to retrieve the authorization token using the silent flow and use this for resources that are protected.
I have a web application running on ASP.NET MVC 5 and a user-managed and backed by Azure AD (i.e. federated user). As a starting point, I have followed the steps in this article: Integrated Windows Authentication.
If I understand correctly, I should be able to use silent authentication since my users are federated and my application is registered as a public application.
In Azure AD, my app is registered with the following properties:
The code is straightforward, from the url.
var app = PublicClientApplicationBuilder.CreateWithApplicationOptions(
new PublicClientApplicationOptions()
{
ClientId = "<clientId>",
TenantId = "<tenantId>",
LogLevel = LogLevel.Verbose,
AzureCloudInstance = AzureCloudInstance.AzurePublic,
})
.Build();
var scopes = new [] { "User.Read" };
var result = await app.AcquireTokenByIntegratedWindowsAuth(scopes)
.WithUsername("<username>")
.ExecuteAsync();
The call for acquiring the token throws the following exception:
"AADSTS65001: The user or administrator has not consented to use the application with ID 'xxx' named 'xxx'. Send an interactive authorization request for this user and resource"
Per the documentation, User.Read doesn't need Admin Consent.
So what am I doing wrong here?
EDIT:
I have constructed a URL that prompts for user consent: https://login.microsoftonline.com/tenantId/oauth2/authorize?client_id=clientId&response_type=code&redirect_uri=<myApp>&nonce=1234&resource=User.Read&prompt=consent
It takes me to the screen where I need to pick my account, and after that, I get redirected to my app, where I get the same exception again.
So it doesn't show any consent screen, just asking me to pick the Microsoft account I want to use. Is this because of consent for User.Read is already granted by the admin?
But why am I still receiving the error? I'm a bit confused at this point.
You're mixing two different AAD OAuth mechanisms (aka v1 and v2 endpoints). The v1 endpoint uses Resources (https://graph.microsoft.com) while the v2 endpoint use Scopes (user.read). So when you request resource=User.Read, you are passing it an invalid resource name.
I would recommend using the v2 endpoint with the following URI prototype (line breaks for readability):
https://login.microsoftonline.com/common/oauth2/v2.0/authorize
?client_id={clientId}
&response_type=id_token
&redirect_uri={your_app}
&response_mode=fragment
&scope=user.read
Because Integrated Windows Authentication is a silent flow, the user of your application must have previously consented to use the application or the tenant admin must have previously consented to all users in the tenant to use the application.
You can force user consent through a URL request with prompt=consent, the url will look like:
https://login.microsoftonline.com/<tenant-id>/oauth2/authorize?client_id=<client id>&response_type=code&redirect_uri=<Your-Redirect-URI-Https-Encoded>&nonce=1234&resource=<your-resource-Https-encoded>&prompt=consent
If it needs admin consent, use $prompt=admin_consent instead.(need to use admin account to sign in)
For more details about fix error AADSTS65001, you could refer to this article.
With delegated permissions, every user will need to consent to the application. If you do not want that requirement, a tenant administrator can consent for all users from the Azure Portal.
How do I create a user using Microsoft graph? For I am having issues with regards to permission failures during a save.
I do have few questions in mind.
Where will the user be created by calling create user API in graph ? Is it in Azure AD or somewhere else ?
I tried calling create user api by passing json and required headers, below is the error I get
Where exactly do I need to set the permission, I have already added permissions in the Application Registration Portal
But when API is executed it shows that I don't have enough permission.
FYI, I have registered the app using the same email id that I am using to test the APIs here https://developer.microsoft.com/en-us/graph/graph-explorer#
If I am not the admin, where exactly do I need to set or request for it ?
In order to create a User via Microsoft Graph, you need to request either Directory.ReadWrite.All or Directory.AccessAsUser.All permission.
Important: Directory.ReadWrite.All and Directory.AccessAsUser.All both require Admin Consent before you can use them. If you're using Graph Explorer then the URI you need to provide your tenant Admin will be generated for you. If you're doing this in your own application, you'll need to construct an Admin Consent URI yourself. You can find more details on this at v2 Endpoint & Admin Consent.
Once you have the proper permissions configured (and consented), you'll want to POST the following JSON body/payload to https://graph.microsoft.com/v1.0/users:
{
"accountEnabled": true,
"displayName": "displayName-value",
"mailNickname": "mailNickname-value",
"userPrincipalName": "upn-value#tenant-name.onmicrosoft.com",
"passwordProfile" : {
"forceChangePasswordNextSignIn": true,
"password": "password-value"
}
}
This will create a user with a temporary password. The user will be asked to set a new password as after as they authenticate for the first time.
Where will the user be created by calling create user API in graph ?
Is it in Azure AD or somewhere else ?
Yes, the user created is in the Azure AD.
I tried calling create user api by passing json and required headers,
below is the error I get
For your error, have you added the request body like the following, and this required admin:
Where exactly do I need to set the permission, I have already added
permissions in the Application Registration Portal
The required permissions to create application:
For the details, please read here.
An existing web application I want to migrate to the Windows Azure Cloud authenticates users the following way somewhere in the (post)authenticaterequest event:
IPrincipal current = Thread.CurrentPrincipal;
if (current != null && ((IClaimsIdentity)current.Identity).Claims.Count > 0)
{
IPrincipal result = AuthManager.CreateGenericPrincipal(current.Identity);
HttpContext.Current.User = result;
Thread.CurrentPrincipal = result;
}
The CreateGenericPrincipal method looks up roles in a xml file for the claimsidentity and creates a new GenericPrincipal with that roles.
Pages that need authentication just perform
IPrincipal p = Thread.CurrentPrincipal;
p.IsInRole("rolesFromXml");
This works fine with one webrole instance since there is no big difference to normal IIS hosting. But will it still work with 2, 3 oder 5 instances? The Azure loadbalancer is not "sticky", users could be forwarded to another instance while using the application. Dunno if Thread.CurrentPrincipal is still the way to go.
I use claims-based identity here. The first time an user enters the page, he gets forwarded to a security token service. Until now, this only happens once. It would be annoying if that happens several times when using multiple instances..
Thanks!
What typically happens is that you are forwarded only once, the redirect dance (passive redirect) happens, and you get a token. The token is typically cached in a cookie in an encrypted format. So, subsequent requests do not do the redirect dance.
The challenge here is that since the cookie is encrypted, all servers in a web farm must have the encryption key to decrypt. Out of box, you will run into issues with WIF because it defaults to DPAPI. This type of encryption is intentionally different per machine. That breaks in the cloud.
What you need to do is upload a service certificate as part of your deployment and change the way the cookie encrypted to something that is webfarm friendly. Here is the magical code:
private void OnServiceConfigurationCreated(object sender,
ServiceConfigurationCreatedEventArgs e)
{
var sessionTransforms =
new List<CookieTransform>(
new CookieTransform[]
{
new DeflateCookieTransform(),
new RsaEncryptionCookieTransform(
e.ServiceConfiguration.ServiceCertificate),
new RsaSignatureCookieTransform(
e.ServiceConfiguration.ServiceCertificate)
});
var sessionHandler = new
SessionSecurityTokenHandler(sessionTransforms.AsReadOnly());
e.ServiceConfiguration.SecurityTokenHandlers.AddOrReplace(
sessionHandler);
}
This sets up your security token handler to use RSA Encryption with key material derived from the installed certificate.
There is more detail and information outlined here in this sample application that illustrates the problem and solution:
http://msdn.microsoft.com/en-us/library/ff966481.aspx
Additional Edit:
There is a pipeline in ASP.NET where WIF is configured. It hooks the authentication event and will pull the token from the cookie and build your IPrincipal so that subsequent code will have that in the context. You typically don't build the Principal yourself when using an STS. Instead, if you need to modify the Principal, you plugin to the pipeline in WIF and insert additional claims to the 'role' claim (actually a URI namespace). WIF will then use those claims to build the ClaimsPrincipal that will contain things like Roles and things just work (IsInRole, web.config auth, etc.).
If possible, it is best to have the token contain the roles as claims. This is a much longer discussion however around 'normalization' of claims to meaningful contexts. Remember, the claims you get from a IP-STS is in their own terms and they might not mean anything to your application. For example, I might get a claim from a customer that they are part of Adatum\Managers group. That is completely meaningless to my application, so what I would typically do is exchange that token for one that my app understands and in the process transform or normalize the claims by claim mappings (i.e. Adatum\Managers --> MyApplicationAdminRole). Windows Azure ACS service is very applicable here to help do that (normalize claims from different IPs).
I would recommend reading Vittorio's book on this all to get the common patterns here:
Eugenio's notes:
Adding to what #dunnry wrote, which is all correct. The proper extensibility point to augment your claim set in the Relying Party (your web app) is by using a ClaimsAuthenticationManager. The docs for this type are here. there are pointers to samples in that page. In that class you would read the roles from the XML file and add them to the ClaimsIdentity. The rest of the app would not worry about claims, etc. (especially if you are using roles like in your case). The RSA config for the cookies encryption solves the load balancer issue.
Look at my post, I just did the same thing.
http://therubblecoder.wordpress.com/2011/10/25/wif-and-load-balancing-with-mvc-3/
Basically the claims token needs to be available to any cluster node, so using a certificate on the sessiontokenhandler will prevent a specific node processing the token in a manner specific to an instance.
In the microsoft.identity element in the config, you need to have an element that looks like this.
<serviceCertificate>
<certificateReference x509FindType="FindByThumbprint" findValue="****THUMBPRINT*****" storeLocation="LocalMachine" storeName="My" />
</serviceCertificate>
The application pool will also need to get access to this otherwise it won't be able to find the certificate by thumbprint.
The above code will use this certicate when dealing with the token. If you don't have this setup you will get a null reference exception.