Cannot add KeyVault Secret-scoped role assignment with Azure Bicep - azure-resource-manager

I am deploying something in the dev ressource group. Something in it has a dependency on a key-vault secret which is stored in a different ressource group main. From the main.bicep I am calling a role-assignment-secret.bicep module to deploy the role assignment:
param role string
param assignee string
param vaultName string
param secretName string
resource secret 'Microsoft.KeyVault/vaults/secrets#2021-11-01-preview' existing = {
name: '${vaultName}/${secretName}'
scope: resourceGroup('main')
}
resource perm 'Microsoft.Authorization/roleAssignments#2020-10-01-preview' = {
name: guid(vaultName, secretName, assignee, role)
properties: {
principalId: assignee
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', role)
}
scope: secret
}
Now secret yields an error, stating that:
A resource's scope must match the scope of the Bicep file for it to be deployable. You must use modules to deploy resources to a different scope.bicep(BCP139)
Than I re-factored the module to include another module
ADD role-assignment.bicep
param role string
param assignee string
resource perm 'Microsoft.Authorization/roleAssignments#2020-10-01-preview' = {
name: guid(deployment().name, assignee, role)
properties: {
principalId: assignee
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', role)
}
}
Which is then called by role-assignment-secret.bicep
param role string
param assignee string
param vaultName string
param secretName string
resource secret 'Microsoft.KeyVault/vaults/secrets#2021-11-01-preview' existing = {
name: '${vaultName}/${secretName}'
scope: resourceGroup('main')
}
module perm 'role-assignment.bicep' = {
name: guid(vaultName, secretName, assignee, role)
scope: secret
params: {
assignee: assignee
role: role
}
}
This then yields the follwing error
Scope "resource" is not valid for this module. Permitted scopes: "resourceGroup".bicep(BCP134)
So basically Bicep is telling me that I cannot assign the role jsut for that specific secret, right? But I need to do this, I can easily do so via the portal GUI. Using a resource group as scope for the role assignment is to broad and results in excessive permissions being granted.

Oh well, there was an obvious oversight on my part. Instead of explicitly providing the scope property WITHIN the role-assignment-secret.bicep template, I should have injected it from outside when calling the module from the main.bicep. Working now. Apologies.

Related

When using Global Query Filters, tenant Id is null when I try to get roles to update the claims

I'm trying to set up multitenancy in the application that I'm working on. I added the Global Query Filters and implemented the TenantProvider as suggested here. Note this block of code in the example that I listed:
public interface ITenantProvider
{
Guid GetTenantId();
}
public class DummyTenantProvider : ITenantProvider
{
public Guid GetTenantId()
{
return Guid.Parse("069b57ab-6ec7-479c-b6d4-a61ba3001c86");
}
}
In my case, instead of the DummyTenantProvider, I have implemented the tenant provider that gets tenantId from the HttpContextAccessor.HttpContext.User.GetTenantId(). I understand that the type of HttpContextAccessor.HttpContext.User is of ClaimsPrincipal, so I added the additional method that accepts this ClaimsPrincipal as parameter and returns tenantId:
public static string GetTenantId(this ClaimsPrincipal principal)
{
return principal.FindFirstValue("tenant");
}
Also, I've implemented the Api Authentication with JWT token. In Startup.cs, I added the registration of the authentication like this:
services.AddAuthentication(options =>
{
// some code that is not relevant
}).AddJwtBearer(options =>
{
// some code that is not relevant
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
// here I get the needed service and from that service, I get the data that I need for Claims (Roles are needed)
var claims = new List<Claim>
{
new Claim(ClaimTypes.Role, JsonConvert.SerializeObject(roles)),
};
var appIdentity = new ClaimsIdentity(claims);
context.Principal.AddIdentity(appIdentity);
},
};
});
Now, the issue that I'm having is when I'm making an HTTP request that's targeting the method in the controller that gets roles for the user (the method is using the same service I'm using in the code above) when debugging the code, first the OnTokenValidated is called and the roles for the user should be populated and added to claims, and then the method in the controller is called. When the OnTokenValidated calls the service and when the request to the database is executed (simple "dbContext.Roles.ToListAsync()" in the repository), the global query filter is applied and tenantId should be added to that database request. But when the global filter is applied, tenantId is null in the GetTenantId method and the code throws ArgumentNullException error. What I can't figure out is why is the tenantId null. My assumption is that when the service is called from OnTokenValidated, that is not the part of the HTTP request, and then the HttpContextAccessor.HttpContext doesn't have the needed value, but I'm not sure if I'm right about that.
I would appreciate some additional insight into what I'm doing wrong here.
If anything else is needed to make things clearer, I'm happy to edit the question.
Thank you all.
For anyone in need of a solution, I was able to resolve this issue by adding the Tenant Id as a new claim in Startup.cs using the TokenValidatedContext (because I have that information in TokenValidatedContext at that moment). Looks something like this:
ctx.HttpContext.User.Identity?.AddClaim(New Claim('tenant'));
Later in the Tenant Provider I have access to this claim.

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();

Assign Delegated permission for System Assigned Managed Identity to Graph Api

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.

ASP.Net Web Api 4.5.2 using UseJwtBearerAuthentication, the Roles are ignored

I would like to use Roles in Azure AD.
I have almost everything working, including Roles configured in the AppRegistration Manifest, etc. On breakpoint, I do see a claim "roles":["MyAdminRole"]
I am trying to apply authorization on the ApiController method:
[System.Web.Http.Authorize("Roles="MyAdminRole")]
public void PostMyMethod(){}
The owin startup looks something like this:
var issuer = "https://sts.windows.net/MyAzureAppServiceGuidFromAuthProperties/";
var secret = TextEncodings.Base64Url.Decode("AzureAppServiceAuthSecret"); //have tried using the encoded value of my Azure Secret here as well
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { "MyAppIdGUID"},
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, secret)
},
TokenValidationParameters = new TokenValidationParameters
{
//this should set ClaimsIdentity.RoleClaimType to "roles", so it finds the "roles" claim during ClaimsPrincipal.UserInRole("")
//in the Mvc Controller example, it behaves this way, but in my ApiController, this is being ignored.
RoleClaimType = "roles"
}
});
The request is coming from an Angular-adal app, and includes a Authorization: Bearer JWT_TOKEN. The token decodes on https://jwt.io, although it says "Invalid Signature" even when I enter my Azure App's Secret from AppService/Auth/AzureAD properties. In the decoded token I do see the audience and issuer are what I expect, and I see the "roles" claim having ["MyAdminRole"].
On a breakpoint in the method, if I inspect the private m_identities
((List<ClaimsIdentity>)typeof(System.Security.Claims.ClaimsPrincipal).GetField("m_identities", System.Reflection.BindingFlags.NonPublic|System.Reflection.BindingFlags.GetField|System.Reflection.BindingFlags.Instance).GetValue(User))[0]
the ClaimIdentity.RoleClaimType values equals System.Security.Claims.ClaimTypes.Role, ie: "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", and User.IsInrole("MyAdminRole") returns false, presumably because the User.Claims role key is "roles", ie: they don't match.
To contrast, from working with jmprieur's azure sample active-directory-dotnet-webapp-roleclaims(https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnet-webapp-roleclaims/), which is using OpenIdConnectAuthenticationOptions, at breakpoints in the Controller methods, the ClaimsIdentity.RoleClaimType has the value "roles", and User.IsInRole("MyAdminRole") returns true, presumably the ClaimsPrincipal.IsInRole is finding the "roles" claim because the ClaimsIdentity.RoleClaimType "roles" matches the User.Claims key of "roles".
How to I get the ClaimsIdentity.RoleClaimType to be "roles" so that ClaimsIdentity.IsInRole("MyAdminRole") to return true so that Authorize("MyAdminRole") will pass? Is it that JwtBearerAuthentication is not constructing the ClaimsPrinciple, something else is, and that something is setting ClaimsIdentity.RoleClaimType to "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"? And, how do I know if the app.useJwtBearerAthentication is even being applied to the request? I don't see any event where I could log that it is being invoked.

How to re-validate token for multi-tenant ASP.NET Identity?

I have implemented a custom OAuthAuthorizationServerProvider to add a domain constraint for the account login. Everything was good. However, I met a problem that, once the user get the token, they can use it for whatever system they want. For example:
They request the TokenEndpointPath with proper username and password (assume it is the admin account of Tenant 1): http://localhost:40721/api/v1/account/auth and receive the Bearer Token.
Now they use it to access: http://localhost:40720/api/v1/info/admin, which is of Tenant 0. The request is considered Authorized.
I tried changing the CreateProperties method but it did not help:
public static AuthenticationProperties CreateProperties(string userName)
{
var tenant = DependencyUtils.Resolve<IdentityTenant>();
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userName", userName },
{ "tenantId", tenant.Tenant.Id.ToString() },
};
return new AuthenticationProperties(data);
}
I also tried overriding ValidateAuthorizeRequest, but it is never called in my debug.
Do I need to implement a check anywhere else, so the Token is only valid for a domain/correct tenant?
(NOTE: a tenant may have multiple domains, so it's great if I can manually perform an account check against correct tenant rather than sticking to a domain. However, it's a plus if I could do that, or else, simply limit the token to the domain is ok)
Not a direct answer to my question (since it's not inside ASP.NET Identity workflow), but the simplest fix I applied was to use ActionFilterAttribute instead.
public class DomainValidationFilter : ActionFilterAttribute
{
public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
// Other Code...
// Validate if the logged in user is from correct tenant
var principal = actionContext.ControllerContext.RequestContext.Principal;
if (principal != null && principal.Identity != null && principal.Identity.IsAuthenticated)
{
var userId = int.Parse(principal.Identity.GetUserId());
// Validate against the tenant Id of your own storage, and use this code to invalidate the request if it is trying to exploit:
actionContext.Response = actionContext.Request.CreateResponse(System.Net.HttpStatusCode.Unauthorized, "Invalid Token");
}
return base.OnActionExecutingAsync(actionContext, cancellationToken);
}
}
Then applies the Filter to all actions by registering it in either FilterConfig or WebApiConfig:
config.Filters.Add(new DomainValidationFilter());

Resources