I have a web api application which I allow an access to only authorized user.
I do it by using attribute [Authorize] with controllers
Can I restrict from accessing the application a particular user with a given username even though he/she's in Azure AD?
Can I restrict from accessing the application a particular user with a given username even though he/she's in Azure AD?
What you need is to create a policy and check current user against this policy whenever you want.
There're two ways to do that.
Use a magic string to configure policy (e.g. [Authorize(policy="require_username=name")]), and then create a custom policy provider to provide the policy dynamically. For more details, see https://learn.microsoft.com/en-us/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-2.2
Create a static policy and use a custom AuthorizeFilter to check whether current user is allowed.
Since this thread is asking "Restricting Azure AD users from accessing web api controller", I prefer to the 2nd way.
Here's an implementation for the 2nd approach. Firstly, let's define a policy of requirename:
services.AddAuthorization(opts =>{
opts.AddPolicy("requirename", pb => {
pb.RequireAssertion(ctx =>{
if(ctx.User==null) return false;
var requiredName = ctx.Resource as string;
return ctx.User.HasClaim("name",requiredName);
});
});
});
And to check against this policy, create a custom AuthorizeFilter as below:
public class RequireNameFilterAttribute : Attribute, IAsyncAuthorizationFilter
{
public string Name{get;set;}
public RequireNameFilterAttribute(string name) { this.Name = name; }
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var user= context.HttpContext.User;
if(user==null){
context.Result = new ChallengeResult();
return;
}
var authZService = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
var result= await authZService.AuthorizeAsync(user, this.Name, "requirename");
if (!result.Succeeded) {
context.Result = new ForbidResult();
}
}
}
Finally, whenever you want to deny users without required names, simply decorate the action method with a RequireNameFilter(requiredName) attribute:
[RequireNameFilter("amplifier")]
public string Test()
{
return "it works";
}
[Edit]
AAD can restrict Azure AD users from accessing web api controller on an Application level. But cannot disallow an user to access a Controller API (API level).
Here's how-to about restricting Azure AD users on an Application Level
Login your Azure portal:
Choose an Activity Directory (e.g. Default Directory)
Click [Enterprise applications]
Choose the application you want to restrict (e.g. AspNetCore-Quickstart)
Select [Properties], Change the [User assignment required] to Yes
Select [Users and groups], Add/Relete users for this application as you need :
Be aware Azure AD is actually an Indentity Provider. This approach only works for the entire application. It's impossible to allow some user to access the App but disallow him to access a specific controller without coding/configuring the Application. To do that, we have no choice but to authorize uses within the application.
Related
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.
I want to develop a SaaS app and deploy it on Azure. Because the business logic will be not so complex so I want to use as a starter kit JHipster. The app will be have two types of users: the "office" users which want to sign into the app using Office 365 account and the "normal" users which want to use their social accounts like Google or Facebook or simply create a new local account. All accounts should be managed by Azure and no password should be stored in our database.
First question is about Azure: which type of AAD should I use? B2B or B2C? Or a mix of both?
Second about JHipster: is it possible to configure JHipster to authenticate users against AAD? Which option should I select in the question about authentication creating the JHipster's app?
Third about Azure: it would be nice if the "office" user could add our SaaS app to the list of apps in the Office 365 main screen. Is it possible?
I have only "on-premise" experience, so maybe my questions are simple but these are my first steps into any clouds, in this case into Azure.
Regards,
Jacek
Pre Requisites: You need to have registered your app in the azure tenent and obtain a client id and secret.
Register App
In your application.yml file settings like these should connect you to azure ad.
# ===================================================================
# OpenID Connect Settings. Default settings are for Azure
# ===================================================================
security:
oauth2:
client:
access-token-uri: https://login.microsoftonline.com/common/oauth2/token
user-authorization-uri: https://login.microsoftonline.com/common/oauth2/authorize
client-id: <<yourclientid>>
client-secret: <<yourregistry>>
client-authentication-scheme: query
preEstablishedRedirectUri: http://localhost:8885/login
useCurrentUri: false
resource:
user-info-uri: https://graph.windows.net/me?api-version=1.6
id: https://graph.windows.net/
You will need to update the UserService class method getUser() to pull down the correct information.
private static User getUser(Map<String, Object> details) {
User user = new User();
user.setId((String) details.get("userPrincipalName"));
user.setLogin(((String) details.get("userPrincipalName")).toLowerCase());
if (details.get("givenName") != null) {
user.setFirstName((String) details.get("givenName"));
}
if (details.get("surname") != null) {
user.setLastName((String) details.get("surname"));
}
if (details.get("displayName") != null) {
user.setDisplayName((String) details.get("displayName"));
}
if (details.get("email_verified") != null) {
user.setActivated((Boolean) details.get("email_verified"));
}
if (details.get("userPrincipalName") != null) {
user.setEmail(((String) details.get("userPrincipalName")).toLowerCase());
}
if (details.get("langKey") != null) {
user.setLangKey((String) details.get("langKey"));
} else if (details.get("locale") != null) {
String locale = (String) details.get("locale");
if (locale.contains("-")) {
String langKey = locale.substring(0, locale.indexOf("-"));
user.setLangKey(langKey);
} else if (locale.contains("_")) {
String langKey = locale.substring(0, locale.indexOf("_"));
user.setLangKey(langKey);
}
}
if (details.get("thumbnailPhoto#odata.mediaEditLink") != null) {
user.setImageUrl((String) details.get("thumbnailPhoto#odata.mediaEditLink"));
}
user.setActivated(true);
return user;
}
Isn't the call to the graph API a GET instead of POST? Did this get changed on later jhipster releases? If so then some more work needs to be done to change the operation. In addition, I don't think the user-info-uri: https://graph.windows.net/me?api-version=1.6 endpoint gives you user roles (AD groups)you would have to make a second call. This of course depends of how your IDP was configured internally.
I published a web app to Azures App Services. I used the App Service's Authentication/Authorization feature to provide security. I successfully added Active Directory features to my web service (and desktop client). It seemed to work very well. Couldn't access data from a browser or desktop client without signing in to the AD.
This was all before I added the [Authorize] attribute to any of the controllers in the API!
So, what will [Authorize] do (or add) to security in my web api. It seems to already be locked up by configuring the Authentication/Authorization features of the web app in Azure.
So, what will [Authorize] do (or add) to security in my web api.
Using ILSpy, you could check the source code about AuthorizeAttribute under System.Web.Mvc.dll. The core code for authorization check looks like this:
protected virtual bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
IPrincipal user = httpContext.User;
if (!user.Identity.IsAuthenticated)
{
return false;
}
if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase))
{
return false;
}
if (_rolesSplit.Length > 0)
{
string[] rolesSplit = _rolesSplit;
IPrincipal principal = user;
if (!rolesSplit.Any(principal.IsInRole))
{
return false;
}
}
return true;
}
The main process would check httpContext.User.Identity.IsAuthenticated, then check whether the current user name, user role is authorized or not when you specifying the allowed Users,Roles.
For Authentication and authorization in Azure App Service(Easy Auth) which is implemented as a native IIS module. Details you could follow Architecture of Azure App Service Authentication / Authorization.
It seemed to work very well. Couldn't access data from a browser or desktop client without signing in to the AD.
This was all before I added the [Authorize] attribute to any of the controllers in the API!
Based on your description, I assumed that you set Action to take when request is not authenticated to Log in with Azure Active Directory instead of Allow Anonymous requests (no action) under your Azure Web App Authentication/Authorization blade.
Per my understanding, you could just leverage App Service Authentication / Authorization which provides built-in authentication and authorization support for you without manually adding middleware in your code for authentication. App service authentication would validate the request before your code can process it. So, for additional custom authorization check in your code, you could define your custom authorize class which inherits from AuthorizeAttribute to implement your custom processing.
public class CustomAuthorize : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//TODO:
}
protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
{
//TODO:
}
}
Then, decorate the specific action(s) or controller(s) as follows:
[CustomAuthorize]
public class UsersController : Controller
{
//TODO:
}
App Service's Authentication/Authorization feature is Based on IIS Level. [Authorize] attribute is based on our code level. Both of this can do Authentication, if you used both of them, it means that there are two levels of authentication in your web app.
Here is a picture that helps you understand them:
For instance, I have an application 'myApp' in app registrations and I have a user in Azure Active Directory User1 which is an Admin in AD. I want to use authentication via Azure AD in that app and want User1 to have a permissions only to access 'api/todos' and 'api/vehicles'. So there can be a role 'Role1with permissionsTodos,Vehicles`. Is there a way to configure that? Thanks.
Yes, there is a way to configure that. Azure works with a Role concept. This sample shows how to do it in a web API.
In short: You need to define the possible roles and assign users to them via the application configuration in the admin portal. And then you need to define which roles the user should be in using the [Authorize] attribute, on the web API controller methods, e.g. like in one of the sample's controllers:
[Authorize(Roles = "Admin, Observer, Writer, Approver")]
public ActionResult Index()
{
ViewBag.Message = "Tasks";
ViewData["tasks"] = TasksDbHelper.GetAllTasks();
return View();
}
As usual with [Authorize] it is inherited, so you can put it on the entire controller class.
Following the steps in this guide Using Azure ACS I have a working Azure ACS service configured & authenticating via Facebook, redirecting back to a website running on my development server.
On authentication success Azure ACS redirects back to my local development website and the IsAuthenticated flag is true, however I want to set the IsAuthenticated flag to true only if the email from the claim also exists in my local database, via a check/call to a custom MembershipProvider. If the email from the claim does not exist I want to redirect the client to a register page. Once registered and authenticated I would like to set the IsAuthenticated flag to true.
Currently once authenticated with Facebook and AzureACS, a user can request a secure page such as ViewAccountBalance.aspx, even though the account does not exist since out of the box IsAuthenticated flag to true. Interested to hear what others have done and what the best practice is.
You'll need to make a clear difference between authentication and authorization. Since the user logged in through Facebook it means he's authenticated (you know who he is and where he comes from).
Now, if you want to restrict parts of the application based on a specific condition you're actually talking about authorization. You might consider combining roles with a simple HttpModule. Example: your HttpModule could verify which page the user is browsing. If the user accesses a page that requires an active profile, you could use the following code:
public class RequiresProfileHttpModule : IHttpModule
{
public void Dispose()
{
}
public void Init(HttpApplication context)
{
context.AuthorizeRequest += new EventHandler(OnAuthorize);
}
private void OnAuthorize(object sender, EventArgs e)
{
HttpApplication app = sender as HttpApplication;
if (app.Request.Url.ToString().Contains("bla") && !app.Context.User.IsInRole("UsersWithProfile"))
app.Response.Redirect("http://myapp/register.aspx");
}
}
The only thing you'll need to take care of is to update the principal to make sure it has the role UsersWithProfile if the user filled in his email address.
This is just one of many possible solutions. If you're using ASP.NET MVC you could achieve the same result with global ActionFilters. Or, you could also try to work with the IClaimsPrincipal (add a claim if the user has a profile).
Sandrino is correct. You can use role based authorization (or more generally, claim based authorization). By default, ACS simply returns the claims issued by the identity providers to your relying party. For Facebook, it will return an email claim. However, you can configure ACS to create additional rules. For example, you can map particular users to a role whose value is administrator. Then ACS will also return this role claim to your relying party. Then you can use Sandrino’s suggestion to use role based authorization. You can also refer to http://msdn.microsoft.com/en-us/library/windowsazure/gg185915.aspx for more information.