Assign Delegated permission for System Assigned Managed Identity to Graph Api - graph

I am trying to setup Managed Identity (system assigned), to assign delegated permission (like Tasks.ReadWrite) and then to use it to call Graph Api.
I have identified object Id using following code:
$app = Get-AzureADServicePrincipal -All $true -Filter "AppId eq '00000003-0000-0000-c000-000000000000'"
$role = $app.Oauth2Permissions | where-Object { $_.AdminConsentDisplayName -eq "Create, read, update, and delete user’s tasks and task lists" }
but when I run following command:
New-AzureADServiceAppRoleAssignment -Id $role.Id -ObjectId $miObjectID -PrincipalId $miObjectID -ResourceId $app.ObjectId
where $miObjectID is my managed identity Id, I am getting following error message:
New-AzureADServiceAppRoleAssignment : Error occurred while executing NewServicePrincipalAppRoleAssignment
Code: Request_BadRequest
Message: Permission being assigned was not found on application

I also tried to do the same. My understanding is, that this is not intended to be.
Typically, user-delegated permissions are supposed to be used interactively, involving user interaction, e.g. on a web-app accessing user resources on his behalf. If, like me, you're developing a backend service, this won't fly. For this scenario the supported way is to just use application permissions and be done with it.
However, imho application permissions are often too broad, granting access to ressources that are irrelevant. E.g. if your app needs to have access to one specific Sharepoint site, you have to authorize access for all sites in your tenant. Application permission cannot be scoped.
Due to compliance reasons this is not acceptable. Especially if you work in a LOB org.
Still, I did find a workaround to have the best of both worlds, i.e. having really scoped permissions and have the ability to leverage these unattended in a backend service. But there is one caveat: I did not get it working with a managed identity I had to use a regular service principal. If that's a compromise you can accept, the following may be helpful to you.
Dedicate a user principal for this scenario. Authorise that user as needed. Choose a password with max length, i.e. 256 chars. Enable MFA.
Create an app/service principal in Azure AD, generate client/app credentials
Create a demo web-app locally using available templates and the MSAL lib. Have the above app request the required user-delegated permissions from the user
Then, in the app code, use the Resource-owner password credential, ROPC flow to authenticate the app and assume the permissions from the user
public class RessourceOwnerPasswordCredentialFlow
{
public static async Task<AccessToken> GetToken(HttpClient http, string credJson, Guid tenantId)
{
var ropc = credJson.Deserialize<GraphROPC>();
var dict = new Dictionary<string, string>();
dict.Add("grant_type", "password"); // ROPC
dict.Add("username", ropc.User);
dict.Add("password", ropc.Password);
dict.Add("client_id", ropc.ClientId);
dict.Add("client_secret", ropc.ClientSecret);
dict.Add("scope", ".default");
var url = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token";
var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = new FormUrlEncodedContent(dict) };
var res = await http.SendAsync(req);
var content = await res.Content.ReadAsStreamAsync();
var authResp = await content.DeserializeAsync<GraphAuthResponse>();
return new AccessToken(authResp.access_token, DateTimeOffset.UtcNow.AddSeconds(authResp.expires_in));
}
}
public class GraphROPC
{
public string User { get; set; }
public string Password { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
}
public class GraphAuthResponse
{
public string token_type { get; set; }
public string scope { get; set; }
public int expires_in { get; set; }
public string access_token { get; set; }
}
Note: Using the ROPC is not recommended by Microsoft.
However, I have found most of the objections to be not applicable in my case.
There is really no alternative to use delegated-permissions within a backend service app
I do have absolute trust in the app, because it's developed by me
The app does have to know the user credentials, these are stored in a KeyVault to which only the app has access (this time using MSI)
Also, one could argue that using two sets of credentials, user credentials plus app credentials is superior to using just the client credentials one would use if working with (more excessive) application permissions.
The ROPC doc says, that user accounts with MFA are not supported. However, I can confirm it is possible to work-around this restriction if conditional access policies are used. In our case the app has a fixed outbound public IP address which can be added as a trusted location. In fact, if the ability to whitelist a trusted location from the MFA requirement were missing, this would have been a blocker for the above steps.

Related

Allow user only access his/her own resource with id in Authorize[] middleare .Net Core Api

I am using role based authentication in .Net Core 3.1 Api. I am using Jwt tokens and user claims. Role based authentication works fine. But in some controllers I want to make sure that user gets his/her own data. Because if an employee sends other employee id in a request he/she can get that resource data, I don't want that.
I have email, id and roles in token with some other data.
What I want is that something like [Authorize(Roles="Employee", Id={userId})]
[HttpGet("getUserInventory")]
//[Authorize(Roles="Employee", Claims.Id={userId})]
public IActionResult getUserInventory([FromQuery] int userId)
{
var inventories = _userInventoryExportService.GetGlobalInventory(userId);
if(inventories.Success)
{
return Ok(inventories.Data);
}
return BadRequest(inventories.Message);
}
Have a look at this tutorial we've created at Curity: Securing a .NET Core API. You will see there how to configure authorization based on claims found in a JWT access token.
had the same use case, to authorize user access to its own mailbox only.
controller:
[HttpPost("{address}/inbox/messages/list")]
[Authorize(Policy = "userAddress")]
public async Task<ActionResult<Response>> ListMessages([FromRoute] string address)
{
// return user mailbox data.
}
here i define the userAddress, and also the way i pull the address string from the url. it is not possible to pass this value from the controller, i had to pick it from a global request class:
//Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("userAddress", policy =>
{
policy.RequireAssertion(context =>
{
var userAddress = context.User.FindFirst(JWTClaim.Email).Value;
// /api/v1/mailbox/email#example.com/inbox/messages/list
var address = new HttpContextAccessor().HttpContext.Request.RouteValues["address"].ToString();
return address == userAddress;
});
});
});
it is worth to note that the context contains the actual request values, but is not publicly accessible, only via debugger:
context.Resource.HttpContext.Request.RouteValues["address"].ToString();

Remove User from Directory Role using Graph API

I am trying to add and remove users from a directory role (Guest Inviter) based on a user's ID. My client ID has Directory.AccessAsUserAll for the Microsoft Graph application. I am using the ID for the directory role and the ID for the user. Using an HTTP Client call (verb is DELETE) I use the format suggested by Microsoft and get an "Insufficient privileges to complete the operation." error. I can perform other functions successfully
It seems clear to me that I am missing something. I would think that you still log in with the Client ID and Client Secret then do something with an admin type id and password rather than just create a new token based these credentials (because then why would you link them) similar to impersonation code but I don't know how and cannot seem to find an example of how.
Using HTTPClient
Verb DELETE
following this pattern
DELETE /directoryRoles/{id}/members/{id}/$ref
https://learn.microsoft.com/en-us/graph/api/directoryrole-delete-member?view=graph-rest-1.0&tabs=cs
Using C# creating bearer token (with client id and client secret) then using an HTTPCLient I call DeleteAsync using a url string based on the recommended pattern.
I see references to needing to pass user credential for a user in an admin role.
I think the issue is the absence of something important. This is called once the bearer token is obtained using client id and client secret for out tenant.
string delURL = $"{settings.RestUrl.value}{settings.RestVersion.value}/directoryRoles/{settings.GuestInviterRoleObjectID.value}/members/{user.id}/$ref";
HttpResponseMessage payload = await client.DeleteAsync(delURL);
Task<string> json = payload.Content.ReadAsStringAsync();
JObject o = new JObject();
if (json.Result.Length > 0)
{
o = JObject.Parse(json.Result);
}
I would like to remove the user from the Guest Inviter directory role. I get however
error: code:"authorization_requestDenied",
messsage: "Insufficient privileges to complete the operation" ....
Update: I was following this example https://dzone.com/articles/getting-access-token-for-microsoft-graph-using-oau-2
I built a class to contain the properties so after getting my original token using Client ID and Client secret then feeding in what I was told was a global admin credentials and now I get a 401 unauthorized error.
string tURL = $"https://login.microsoftonline.com/{settings.TenantID.value}/oauth2/token";
using (System.Net.WebClient c = new System.Net.WebClient())
{
c.Headers["Authorization"] = $"Bearer {token}";
c.Headers["Content-Type"] = "application/x-www-form-urlencoded";
System.Collections.Specialized.NameValueCollection data = new System.Collections.Specialized.NameValueCollection();
body.GetType().GetProperties().ToList().ForEach(delegate (System.Reflection.PropertyInfo item)
{
data.Add(item.Name, item.GetValue(body) == null ? string.Empty : item.GetValue(body).ToString());
});
var res = await Task.Run(() => c.UploadValues(tURL, data));
Task.WaitAll();
if(res != null)
{
string response = System.Text.Encoding.UTF8.GetString(res);
}
}
Data object
public class JSONBody
{
public string grant_type { get; set; }
public string client_id { get; set; }
public string client_secret { get; set; }
public string resource { get; set; }
public string username { get; set; }
public string password { get; set; }
public JSONBody()
{
this.grant_type = "password";
this.resource = "https://graph.microsoft.com";
}
}
I cannot prove or disprove the 401 error because I cannot prove my code works (or doesn't).
According to the documentation https://learn.microsoft.com/en-us/graph/api/directoryrole-delete-member
You’ll need an application with the delegated Directory.AccessAsUser.All permission. The you’ll need an admin to login to that application (with the correct permissions).
The application credentials (or client credentials flow) is unsupported, by design.
This could result in privilege elevation, if some admin would create an application with these permissions. If that admin would then be removed from the admin role he would be able to use his application to make himself admin again

Complex authentication with existing user database in MVC5

I'm migrating a SaaS app from Classic ASP to .NET MVC5 and will use EF6 Database First. The login form for end users is customisable by each tenant (on their own subdomain but pointing to the same web application). We wish to use the existing database schema and the new authentication & authorization filters.
For example, a user on one tenant may login by entering their first name, surname and a code generated by our system. A user on another tenant may login by entering their email address and a password. Additionally, each tenant has a separate administrator login which uses a username and password. Another tenant may use LDAP authentication against a remote AD server.
Is there a definitive best practice way of doing custom authentication?
Almost every article appears to suggest different ways of accomplishing this: simply setting FormsAuthentication.SetAuthCookie, using a custom OWIN provider, override AuthorizeAttribute, etc.
In Classic ASP, we queried the database to find out the type of login for that tenant, displayed the appropriate fields on the login screen and then on post back, checked the fields match what's in the database and then set the session variables appropriately which were checked on each page request.
Thanks
I find that Identity framework is very flexible in terms of authentication options. Have a look on this bit of authentication code:
var identity = await this.CreateIdentityAsync(applicationUser, DefaultAuthenticationTypes.ApplicationCookie);
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
This is pretty standard run of the mill authentication part in Identity, you'll find this in every Identity sample on the web. If you look closely it is very flexible - all you need for authentication is ApplicationUser object that framework does not care how you get.
So in theory you can do things like this (pseudocode, I did not try to compile this):
// get user object from the database with whatever conditions you like
// this can be AuthCode which was pre-set on the user object in the db-table
// or some other property
var user = dbContext.Users.Where(u => u.Username == "BillyJoe" && u.Tenant == "ExpensiveClient" && u.AuthCode == "654")
// check user for null
// check if the password is correct - don't have to do that if you are doing
// super-custom auth.
var isCorrectPassword = await userManager.CheckPasswordAsync(user, "enteredPassword");
if (isCorrectPassword)
{
// password is correct, time to login
// this creates ClaimsIdentity object from the ApplicationUser object
var identity = await this.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
// now we can set claims on the identity. Claims are stored in cookie and available without
// querying database
identity.AddClaim(new Claim("MyApp:TenantName", "ExpensiveClient"));
identity.AddClaim(new Claim("MyApp:LoginType", "AuthCode"));
identity.AddClaim(new Claim("MyApp:CanViewProducts", "true"));
// this tells OWIN that it can set auth cookie when it is time to send
// a reply back to the client
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
Using this authentication, you have set a few claims on the user - they are stored in the cookie and available everywhere via ClaimsPrincipal.Current.Claims. Claims are essentially a collection of key-value pairs of strings and you can store there anything you like.
I usually access claims from the user via extension method:
public static String GetTenantName(this ClaimsPrincipal principal)
{
var tenantClaim = principal.Claims.FirstOrDefault(c => c.Type == "MyApp:TenantName");
if (tenantClaim != null)
{
return tenantClaim.Value;
}
throw new ApplicationException("Tenant name is not set. Can not proceed");
}
public static String CanViewProducts(this ClaimsPrincipal principal)
{
var productClaim = principal.Claims.FirstOrDefault(c => c.Type == "MyApp:CanViewProducts");
if (productClaim == null)
{
return false;
}
return productClaim.Value == "true";
}
So in your controller/view/business layer you can always call to ClaimsPrincipal.Current.GetTenantName() and in this case you'd get "ExpensiveClient" back.
Or if you need to check if a specific feature is enabled for the user, you do
if(ClaimsPrincipal.Current.CanViewProducts())
{
// display products
}
It is up to you how you store your user properties, but as long as you set them as claims on the cookie, they will be available.
Alternatively you can add claims into the database for every user:
await userManager.AddClaimAsync(user.Id, new Claim("MyApp:TenantName", "ExpensiveClient"));
And this will persist the claim into the database. And by default, Identity framework adds this claim to the user when they login without you needing to add it manually.
But beware, you can't set too many claims on a cookie. Cookies have 4K limit set by browsers. And the way Identity cookie encryption works it increases encoded text by about 1.1, so you can have roughly 3.6K of text representing claims. I've run into this issue here
Update
To control access to controllers via claims you can use the following filter on the controller:
public class ClaimsAuthorizeAttribute : AuthorizeAttribute
{
public string Name { get; private set; }
public ClaimsAuthorizeAttribute(string name)
{
Name = name;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
var user = HttpContext.Current.User as ClaimsPrincipal;
if (user.HasClaim(Name, Name))
{
base.OnAuthorization(filterContext);
}
else
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary()
{
{"controller", "errors"},
{"action", "Unauthorised"}
});
}
}
}
and then use this attribute on controllers or separate actions like this:
[ClaimsAuthorize("Creating Something")]
public ActionResult CreateSomething()
{
return View();
}
User will require "Create Something" claim on them to access this action, otherwise they will be redirected to "Unauthenticated" page.
Recently I've played with claims authentication and made a prototype application similar to your requirement. Please have a look on the simple version: https://github.com/trailmax/ClaimsAuthorisation/tree/SimpleClaims where claims are stored individually for each user. Or there is more complex solution where claims belong to a role and when users login, role claims assigned to the user: https://github.com/trailmax/ClaimsAuthorisation/tree/master
There's two components you need. The authentication itself and the strategy each user gets for authentication.
The first is easy and is accomplished with these two lines...
var identity = await UserManager.CreateIdentityAsync(user,
DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties()
{ IsPersistent = isPersistent }, identity);
When a user is Signed In, they get an identity which contains the user's claims on roles and who they are. These are given to the user as a cookie. After this point you just decorate controllers with [Authorize] to make sure only authenticated users can log in. Pretty standard here.
The only complicated part in the problem is the second part; The strategy for how each user gets authenticated set by the admin.
Some pseudocode for how this could work in actions is this...
// GET: /Account/Login
[AllowAnonymous]
public ActionResult Login(int tenantId)
{
var tenant = DB.GetTenant(tenantId);
return View(tenant);
}
In your view you would output the authentication strategy for the tenant. That may be email and password, a code and email, or whatever your requirements.
When the user enters their info and clicks to login, you then have to determine what strategy they were using, and check to see if their information matches.
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model)
{
var tenant = DB.GetTenant(model.tenantId);
//If user info matches what is expected for the tenants strategy
if(AuthenticateUserInfo(tenant, model.UserInputs))
{
//Sign the user in
var identity = await UserManager.CreateIdentityAsync(user,
DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties()
{ IsPersistent = isPersistent }, identity);
}
}
I did a lot of hand-waving in the second part because of the complicated nature of how dynamic it is. Overall you should use the same strategies you used in your legacy application to generate the right inputs and such. Nothing has changed there, only the way you sign in is going to be different.
Using Visual Studio 2013 Update 3 you can create a new Web Application that comes with MVC5, EF6 and Identity already installed. Here is how to select Identity when you create a new Application:
With MVC Template selected, click Change Authentication and the highlighted window will pop up. Individual User Accounts = Identity. Click ok and continue.
Having done that, you have created an application with Identity. You can now customize your login and registration as follows.
You want to look at your AccountController.cs in the Controllers folder. Here you will find the script for Registration and Login.
If you look at the
public async Task<ActionResult> Register(RegisterViewModel model)
function, you'll notice it contains:
IdentityResult result = await UserManager.CreateAsync(new ApplicationUser() { UserName = newUser.UserName }, newUser.Password);
This is where the user gets created. If you want to use Identity, you should save the users username and password. You can use an e-mail as the username if you want. etc.
After doing that, I add the user a specified role (I find the user and then add it to the role):
ApplicationUser userIDN = UserManager.FindByName(newUser.UserName);
result = await UserManager.AddToRoleAsync(userIDN.Id, "Admin");
In my scenario, I have created an additional extended table where I hold their address, phone number, etc. In that table, you can hold any additional login information. You can add these new entries before or after creating the users account in Identity. I would create the extended information and then create the Identity account just to be sure.
IMPORTANT: For any scenarios where a user is logging in with something that is not a username or e-mail address that isn't saved into via Identity, you will have to do a custom solution.
Example: User types in their first name, surname and the code. You could do two things: Save the first name and surname into the username field of identity and the code into the password and verify the login that way
OR
you would check your custom table for those properties and make sure they match, if and when they do you could call this little beauty:
await SignInAsync(new ApplicationUser() { UserName = model.UserName }, isPersistent: false);
Once you call that SignInAsync function, you can go ahead and direct them to your protected page.
NOTE: I'm creating the ApplicationUser on the function call but if you use it more than once it would be ideal for you to declare the ApplicationUser as follows:
ApplicationUser user = new ApplicationUser() { UserName = model.UserName };
NOTE #2: If you don't want to user Async methods, those functions all have non-async versions of them.
Note #3: At the very top of any page using UserManagement, it is being declared. Make sure if you are creating your own controller that wasn't generated by Visual Studio to use Identity, you include the UserManagement declaration script at the top inside of the class:
namespace NameOfProject.Controllers
{
[Authorize]
public class AccountController : Controller
{
public AccountController() : this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()))) { }
public AccountController(UserManager<ApplicationUser> userManager) { UserManager = userManager; }
public UserManager<ApplicationUser> UserManager { get; private set; }
Please let me know if you have any questions and I hope this helps.

Can ServiceStack routes not handle special characters in their values?

I'm developing an API for our business.
Requests should require authTokens that require a POST http verb to retrieve. The flow should work like this-
User's client POSTS username and password (ssl protected) to the GetAuthToken service. Service returns auth token.
User's client can use token in any other request, no longer requiring POST.
I've written a convenience rest function, CheckAuthToken, to allow users to debug whether their working auth token is correct. It requires the email address of the user and the auth token to check. This works fine in SOAP and via POST, but the route doesn't seem to work via GET.
Here's the DTO:
[Route("/auth/CheckAuthToken/{EmailAddress}/{AuthToken}")]
public class CheckAuthToken
{
public string EmailAddress { get; set; }
public string AuthToken { get; set; }
}
If I request, for example (note the %2E, at the recommendation of this post: ServiceStack Handler Not Found When Periods Present in Path):
GET /auth/CheckAuthToken/aaron%40meemailsite%2Ecom/Hnot0real0auth0token4mzSBhKwFXY6xQcgX6XqsE%3D HTTP/1.1\r\n
I still get a 404. It seems like the period is being decoded before handing off to ServiceStack.
I recognize that I could use query string variables instead, making my request this:
GET /auth/CheckAuthToken?EmailAddress=aaronb%40bluebookinc%2Ecom&AuthToken=HwjQoKiHD2HSngFeeCH1k4mzSBhKwFXY6xQcgX6XqsE%3D HTTP/1.1\r\n
But, I was hoping to be more flexible than that, especially in my business layer REST services, which will also require user identification via email on certain functions.
Any ideas?
What version of ServiceStack are you running?
This is a passing test in the latest version (3.9.55). I also tested with a simple API with your endpoints and was having no problems passing those values in the url.
[Route("/auth/CheckAuthToken/{EmailAddress}/{AuthToken}")]
public class CheckAuthToken : IReturn
{
public string EmailAddress { get; set; }
public string AuthToken { get; set; }
}
[Test]
public void Test()
{
var url = new CheckAuthToken() {
EmailAddress = "me#test.com",
AuthToken = "fake"
}.ToUrl("GET");
Assert.That(url, Is.EqualTo("/auth/CheckAuthToken/me%40test.com/fake"));
}

How to securely store service passwords with ASP.NET Membership?

I am using ASP.NET MVC 3 membership for my site with defaults. Thus the membership password for the user is stored securely.
My service requires the user to enter username/passwords for other web services they use. I access those services from my service. If I save the users' service passwords I need to ensure that those usernames/passwords are stored securely as well so that if someone hacks my server they will not be exposed.
I understand the general concepts of how this might be done (encrypt the username/pw using the hash of the ASP.NET membership pw they've provided as the key). But I don't know the specific APIs or correct patterns.
I also think it's basically impossible to really do this because if someone hacks my service they could simply use the hash themselves to decrypt the passwords. Am I right about that.
Assuming i'm wrong, and it is possible to do what I want, assume my model contains something like this:
public class MSExchangeSettings
{
[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "Email address for your Exchange account")]
public string EmailAddress { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "Password for your Exchange account")]
public string Password { get; set; }
...
}
Can someone please provide an example of how to do this correctly?
If, this is not possible I'll have to resort to asking the user for the PW every every time which I want to avoid. Of course, for the services I use that support OpenID or OAuth I have other alternatives, but for this specific example (Exchange) I need a username/pw.
Take a look at this
private static string GetPasswordHashed(string password) {
var saltBytes = new byte[0x10];
using (var random = new RNGCryptoServiceProvider()) {
random.GetBytes(saltBytes);
}
var passwordBytes = Encoding.Unicode.GetBytes(password);
var combinedBytes = saltBytes.Concat(passwordBytes).ToArray();
byte[] hashBytes;
using (var hashAlgorithm = HashAlgorithm.Create("HashAlgorithm")) {
hashBytes = hashAlgorithm.ComputeHash(combinedBytes);
}
var PasswordHashed = Convert.ToBase64String(hashBytes);
return PasswordHashed;
}

Resources