I'm developing a quiz app which requires authorization for only-subscribed members can see.
How to do that? I'm thinking of putting metadata (is_subscribed) to true for subscribed member and give the scope so he/she can gain permissions.
But, I don't know how to do it. Please help. The docs is so confusing
There are two separate concerns here.
Where to keep the subscription information. app_metadata is fine, or you might choose to do so in a backend database (application specific). A client application will probably handle subscriptions and be in charge of updating that value. If you store the value in app_metadata, you will use Management API v2 to alter the user profile from the application that handles subscriptions.
Add an authorization scope based on the subscription status. In this case, you would use a rule to add a custom scope based on the value of the is_subscribed field. I.e.:
function(user, context, callback) {
if (user.app_metadata && user.app_metadata.is_subscribed) {
context.accessToken.scope = ['read:quiz'];
} else {
// remove the ability to read a quiz if not subscribed.
context.accessToken.scope = [];
}
callback(null, user, context);
}
If you decided to store the subscription information in a backend database instead of in the app_metadata, you would simply access the database from the rule in the above code.
Related
I am designing a REST backend in Nest.js that needs to allow Users to be a part of multiple Organizations. I want to use role-based access control, such that a user can have one or more named roles. Crucially, these roles need to be able to be either "global" (not dependent on any organization, ex. SUPERUSER), or "scoped" (specific to an organization, ex. MANAGER).
I have decided on this basic database design, which links Users to Organizations using the Roles table in a many-one-many relationship:
As you can see, the organizationId field on a Role is optional, and if it is present, then the user is linked to that organization through the role. If it is not present, I assume this to be a "global" role. I find this to be an elegant database design, but I am having trouble implementing the guard logic for my endpoints.
The guard logic would go something like this:
Look up all the Roles from the database that match the current userId.
For global routes, check that at least one of the returned roles is in the list of required roles for the route.
For scoped routes, do the same, but also check that the organizationId of the role matches the organization ID associated with the operation (I'll elaborate below).
Consider these two endpoints for Jobs. The first will retrieve all the jobs associated with a specified organization. The second will find a single job by its id:
Example route 1:
GET /jobs?organizationId=XXXXX
#Roles(Role.MANAGER, Role.EMPLOYEE)
#UseGuards(JwtAuthGuard, RolesGuard)
#Get()
getMyJobs(#Query() query: {organizationId: string}) {
return this.jobsService.getJobs({
organizationId: query.organizationId,
})
}
Example route 2:
GET /jobs/:jobId
#Roles(Role.MANAGER, Role.EMPLOYEE)
#UseGuards(JwtAuthGuard, RolesGuard)
#Get(':jobId')
getJob(#Param('jobId') jobId: string) {
return this.jobsService.getJob(jobId)
}
In the first example, I know the organizationId without doing any work because it is required as a query parameter. This id can be matched against the id specified in the Role. This is trivial to validate, and ensures that only users who belong to that organization can access the endpoint.
In the second example, the organizationId is not provided. I can easily query it from the database by looking up the Job, but that is work that should be done in the service/business logic. Additionally, guard logic executes before getJob. This is where I am stuck.
The only solution I can come up with is to pass the organizationId in every request, perhaps as a url parameter or HTTP header. Seems like there should be a better option than that. I'm sure this pattern is very common, but I don't know what it is called to do any research. Any help regarding this implementation would be greatly appreciated!
It is just another option for you.
You can modify a user object inside RolesGuard by adding a field that stores available organizations for him/her. So you need to calculate organizations for user, who makes a request inside a guard and then put a result array with ids of organizations to a user field (user.availableOrganizationIds = []). And then use it for filtering results
#Roles(Role.MANAGER, Role.EMPLOYEE)
#UseGuards(JwtAuthGuard, RolesGuard)
#Get()
getMyJobs(#User() user) { // get a user from request
return this.jobsService.getJobs({
organizationIds: user.availableOrganizationIds, // <<- filter by organizations
})
}
This is regarding an application where we are using Azure B2C tenant for authentication. There is a requirement to get lists of users which would support filtering, pagination and users have to be from a particular TenantId. We are using Graph API SDK that i.e., microsoft.graph and microsoft.graph.Auth packages.
Steps I have done
Created graph client with ClientCredentialProvider with TenantId.
Getting users using the below code
var users = await graphClient.Users
.Request()
.Top(100)
.Filter("identities/any(c:c/issuer eq 'contoso.onmicrosoft.com')")
.Select("displayName,id,identities")
.GetAsync();
This gets all users for a given issuer or tenant. Now, there is an issue I cannot filter users using this option .Filter("identities/any(c:c/issuer eq 'contoso.onmicrosoft.com') and startswith(displayName,'a') i.e., get all users whose display name starts with 'a' and belong to this issuer 'contoso.onmicrosoft.com'. As per Microsoft, Graph API does not currently support complex queries on Identities. They show this message Message: Complex query on property identities is not supported.
Now, right now my thoughts are limited to this option of loading entire user table for this tenant onto memory. I think this would be not the best approach, because we will have more tenants and I don't know how much users we can store in memory.
Anyone who has more understanding on these type of scenarios, please share your inputs. I wanted to know various other alternatives we could take.
As the message says, "Complex query on property identities is not supported", it's also not supported in Microsoft Graph SDK. You could only check the users with String.StartsWith() method in C#.
var users = await graphClient.Users
.Request()
.Top(100)
.Filter("identities/any(c:c/issuer eq 'contoso.onmicrosoft.com')")
.Select("displayName,id,identities")
.GetAsync();
List<User> userResult = new List<User>();
foreach(var user in users)
{
if (user.displayName.StartsWith(a)) {
userResult.add(user);
}
}
We're developing an application that uses a back-end built on .Net Core 2.2 Web API. Most of our controllers merely require the [Authorize] attribute with no policy specified. However, some endpoints are going to require the user to be in a particular Azure AD Security Group. For those cases, I implemented policies like this in the Startup.cs file:
var name = "PolicyNameIndicatingGroup";
var id = Guid.NewGuid; // Actually, this is set to the object ID of the group in AD.
services.AddAuthorization(
options =>
{
options.AddPolicy(
name,
policyBuilder => policyBuilder.RequireClaim(
"groups",
id.ToString()));
});
Then, on controllers requiring this type of authorization, I have:
[Authorize("PolicyNameIndicatingGroup")]
public async Task<ResponseBase<string>> GroupProtectedControllerMethod() {}
The problem is that our users are all in a large number of groups. This causes the Graph API to return no group claims at all, and instead a simple hasGroups boolean claim set to true. Therefore, no one has any groups, and thus cannot pass authorization. This no-groups issue can be read about here.
This string-based policy registration, lackluster as it may be, seems to be what the .Net Core people are recommending, yet it falls flat if the groups aren't populated on the User Claims. I'm not really seeing how to circumnavigate the issue. Is there some special way to set up the AppRegistration for my API so that it does get all of the groups populated on the User Claims?
Update:
In the solution, I do have a service that calls Graph to get the user's groups. However, I can't figure out how to call it before it's too late. In other words, when the user hits the AuthorizeAttribute on the controller to check for the policy, the user's groups have not yet been populated, so the protected method always blocks them with a 403.
My attempt consisted of making a custom base controller for all of my Web API Controllers. Within the base controller's constructor, I'm calling a method that checks the User.Identity (of type ClaimsIdentity) to see if it's been created and authenticated, and, if so, I'm using the ClaimsIdentity.AddClaim(Claim claim) method to populate the user's groups, as retrieved from my Graph call. However, when entering the base controller's constructor, the User.Identity hasn't been set up yet, so the groups don't get populated, as previously described. Somehow, I need the user's groups to be populated before I ever get to constructing the controller.
I found an answer to this solution thanks to some tips from someone on the ASP.NET Core team. This solution involves implementing an IClaimsTransformation (in the Microsoft.AspNetCore.Authentication namespace). To quote my source:
[IClaimsTransformation] is a service you wire into the request pipeline which will run after every authentication and you can use it to augment the identity as you like. That would be where you’d do your Graph API call [...]."
So I wrote the following implementation (see an important caveat below the code):
public class AdGroupClaimsTransformer : IClaimsTransformation
{
private const string AdGroupsAddedClaimType = "adGroupsAlreadyAdded";
private const string ObjectIdClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier";
private readonly IGraphService _graphService; // My service for querying Graph
private readonly ISecurityService _securityService; // My service for querying custom security information for the application
public AdGroupClaimsTransformer(IGraphService graphService, ISecurityService securityService)
{
_graphService = graphService;
_securityService = securityService;
}
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var claimsIdentity = principal.Identity as ClaimsIdentity;
var userIdentifier = FindClaimByType(claimsIdentity, ObjectIdClaimType);
var alreadyAdded = AdGroupsAlreadyAdded(claimsIdentity);
if (claimsIdentity == null || userIdentifier == null || alreadyAdded)
{
return Task.FromResult(principal);
}
var userSecurityGroups = _graphService.GetSecurityGroupsByUserId(userIdentifier).Result;
var allSecurityGroupModels = _securityService.GetSecurityGroups().Result.ToList();
foreach (var group in userSecurityGroups)
{
var groupIdentifier = allSecurityGroupModels.Single(m => m.GroupName == group).GroupGuid.ToString();
claimsIdentity.AddClaim(new Claim("groups", groupIdentifier));
}
claimsIdentity.AddClaim(new Claim(AdGroupsAddedClaimType, "true"));
return Task.FromResult(principal);
}
private static string FindClaimByType(ClaimsIdentity claimsIdentity, string claimType)
{
return claimsIdentity?.Claims?.FirstOrDefault(c => c.Type.Equals(claimType, StringComparison.Ordinal))
?.Value;
}
private static bool AdGroupsAlreadyAdded(ClaimsIdentity claimsIdentity)
{
var alreadyAdded = FindClaimByType(claimsIdentity, AdGroupsAddedClaimType);
var parsedSucceeded = bool.TryParse(alreadyAdded, out var valueWasTrue);
return parsedSucceeded && valueWasTrue;
}
}
Within my Startup.cs, in the ConfigureServices method, I register the implementation like this:
services.AddTransient<IClaimsTransformation, AdGroupClaimsTransformer>();
The Caveat
You may have noticed that my implementation is written defensively to make sure the transformation will not be run a second time on a ClaimsPrincipal that has already undergone the procedure. The potential issue here is that calls to the IClaimsTransformation might occur multiple times, and that might be bad in some scenarios. You can read more about this here.
You can use the Microsoft Graph API to query the user's groups instead:
POST https://graph.microsoft.com/v1.0/directoryObjects/{object-id}/getMemberGroups
Content-type: application/json
{
"securityEnabledOnly": true
}
Reference: https://learn.microsoft.com/en-us/graph/api/directoryobject-getmembergroups?view=graph-rest-1.0&tabs=http
The scenario will be:
Your client app will acquire access token (A) for accessing your back-end Web API.
Your Web API application will acquire access token (B) for accessing the Microsoft Graph API with the access token (A) using OAuth 2.0 On-Behalf-Of flow. Access token (B) will be used to get the user's groups.
Web API validates the user's group using a policy (recommended) or custom attribute.
The protocol diagram and sample request are listed in this article using the Azure AD V2.0 Endpoint. This article is for the V1.0 endpoint. Here are code samples for .Net Core.
Suppose I have two users, A and B.
A is allowed to access all the resources available, but B can access them only partially.
What is the proper way to prevent B from accessing resources to which B does not have permission?
Should I create some sort of whitelist that specifies only URLs B can access?
The following snippet is what I currently have.
It is a middleware which checks whether each request is allowed to access specific URLs.
const ALLOWED_URLS = ['api/resource1', 'api/resource2', 'api/resource3'];
const sessionCheck = (req, res, next) => {
const url = req.originalUrl;
// check whether accessing URL is allowed
}
Is there any better approach than this?
What you're asking about is called IAM (Identity & Access Management).
The roles, ownership, and permissions on specific resources is generally persisted in your database as business domain objects of their own. This is language agnostic and not specific to node or express. You should NOT build a white list of URLs. The resources you want to protect are in your database. You should map them to permission objects that in turn map to users. Your not protecting URLs, your protecting resources. Everyone is allowed to access any URL but the resources behind them are what you're protecting and those rules/permissions go in your database.
If your looking for industry standards then here are some common names/terms for those IAM objects that would be persisted in your DB:
Group
Role
User
Policy
User A's access would generally be determined by what role they have or what group they belong to. Whether you give user A a role, put them in an authorized group, or give them a direct permission doesn't really matter, these groupings exist to reduce duplication so you can pass around or take away multiple permissions at once. But the general idea is the same; your resources exist in a DB and you specify what is the required or allowed roles, groups, and users that can access those resources and you map users to groups, roles, etc as simple table entries. This means the real authorization logic is not in Node or Express or even in your webapp, its built into the resources themselves and tied to how the data is retrieved.
Resource Retrieval Code
When anyone makes a request for a given resource the query, regardless of your database type, should fail if the user is not authorized. This means the way you are retrieving data must be directly tied to how its authorized and not two separate steps; meaning you should not get the resource, then check if the user is authorized and you should not check if the user is authorized before getting the resource. The best practice is fuse/join the two so that you can not get the resource unless your authorized because we look for the resource using your role and if you don't have the right role we can't find the resource.
For example:
function getAccount(userId,accountId) {
makeSQLCall(userId,accountId)
}
SELECT *
FROM accounts a
WHERE a.accountId = accountId AND u.userId = userId
JOIN users u ON a.allowedRole = u.role
The SQL doesn't matter as the same can be done with other technologies but the last line is the most important (account.allowedRole = user.role). You literally pull the resource from the database using the users role so that if they are not authorized this fails and no data is returned. This is also your base/parent data retrieval function so other functions that don't know about authorization can use this function and authorization will be dealt with under the hood.
Express Pseudo Code
router.get('/api/resource1',function(req,res){
var user = utility.getUserFromRequest(req)
var resource = accountService.getAccount(user,req.body.accountId)
sendResponse(resource)
})
Looking at the code above the authorization is built into your domain model not your web application. If the user making the request above is not authorized they will get no data back. You have to figure out in your own business use case is it enough to return an empty result or do you need to return a 401 HTTP error code. If you need to inform your non-malicious users they are not authorized you can simply perform isAuthoriized(user,accountId) before running accountService.getAccount as a UI convenience. The strength of this approach is that if you or some other developer forgets to check isAuthorized 1st the call will still return no data since isAuthorized() is just for the users benefit and not security. The security is at the domain/db layer.
Typically you would build user resource routes and validate their ownership when you authenticate and their permissions on authorization.
E.g.
server.get('api/v0/products/:user', authMiddleware, (req, res, next) => {
// from the auth middleware comes a parsed token with payload
if (req.payload.user !== req.params.user) {
return res.status(401).send('not allowed')
}
// do action
})
An alternative would be to only access database objects that matches the payload of your authentication
The example would suggest using JWT middleware with a custom property user
I am developing a mobile app in which I need to authenticate a user against Azure AD. Basically the user will be prompted their organisational email and password, which the mobile phone app sends to the backend server which will authenticate.
I have the 'public-client-app-sample' of 'azure-activedirectory-library-for-java' working, and can authenticate against 'graph.windows.net':
private static AuthenticationResult getAccessTokenFromUserCredentials(
String username, String password) throws Exception {
AuthenticationContext context = null;
AuthenticationResult result = null;
ExecutorService service = null;
try {
service = Executors.newFixedThreadPool(1);
context = new AuthenticationContext(AUTHORITY, false, service);
Future<AuthenticationResult> future = context.acquireToken(
"https://graph.windows.net", CLIENT_ID, username, password,
null);
result = future.get();
} finally {
service.shutdown();
}
if (result == null) {
throw new ServiceUnavailableException(
"authentication result was null");
}
return result;
}
However, this does not return any userInfo (is null), and I can't figure out at this moment how to query to get a list with groups the user belongs to?
Do I just do manual lookups using the API using the tokens obtained from Adal4j, or is there a provided function within the library?
I am only starting with Azure, so it might be obvious, I might just be looking in the wrong places. I tried e.g. 'https://graph.windows.net/xxx.com/groups?api-version=1.5' but get 'Resource 'https://graph.windows.net/xxx.com/groups?api-version=1.5' is not registered for the account.'
First, you're absolutely right, adal4j was failing to return UserInfo. The reason for this was that the token request did not include the scope=openid parameter, which is required if the caller wants an id_token in the response. I opened an issue, and it has already been resolved. So, an updated copy of adal4j will fix your first issue of not getting UserInfo.
Now, regarding group membership for the current user: Normally, I would recommend that you simply configure you application to return the groups claim. This can be done very easily by changing the application manifest (downloaded and uploaded via the Azure portal, under the Application's configuration page) to include:
"groupMembershipClaims": "All",
Unfortunately, adal4j does not yet include the groups claim in the result of getUserInfo(), so that probably won't work much for you (issue opened, it really depends on how quickly it gets implemented, or if you want to implement youself).
Regardless, because it is possible for there to be too many groups to include in the token (indicated by , your application should always be able to use the AAD Graph API to retrieve the full set of groups the user is a member of.
And that brings me to the last point: adal4j does not implement a client of the Azure AD Graph API. So yes, you would have to implement that yourself (or perhaps use/modify an existing OData client).
(You can read more about Group Claims in this Azure AD blog post.)