We have a controller that we have been developing that uses the Microsoft.Identity.Web.AuthorizeForScopes attribute for the controller for example
[AuthorizeForScopes(Scopes = new string[]
{
"https://ourdomain.co.uk/us.theapi/access",
})]
public class BrowseController : Controller
This has worked well until we now. We need to deploy the set of web apis to production. The apis will have a new Azure AD application registration for production. That will mean we'll need to "Add a scope" in the "Expose an API" section of the Azure Portal. The new Azure AD application has a new Application ID URI (Azure does not allow it to use the one that was used by the devlopment application registration).
That App ID URI forms part of the scope string so if our new app id is "https://ourdomain.co.uk/us.prod-theapi/" and the scope is still "access" then the attribute in the code would need to be "https://ourdomain.co.uk/us.prod-theapi/" however that is no good for development.
Is there a way of checking for scopes that gets a similar result to the one obtained by AuthorizeForScopes attribute but without using an Attribute? We need to be able to drive the scope string used by a configuration property
Based on the source code, you can actually define a configuration section key instead of defining the scopes inline.
E.g.:
[AuthorizeForScopes(ScopeKeySection = "Auth:Scopes")]
public class BrowseController : Controller
And in appsettings JSON:
{
"Auth": {
"Scopes": "https://ourdomain.co.uk/us.theapi/access"
}
}
You can then have a different value for that configuration value depending on the environment or through environment variables etc.
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 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.
I have set up two App Services in Azure. 'Parent' and 'Child', both expose API endpoints.
Child has endpoint 'Get'.
Parent has endpoints 'Get' and 'GetChild' (which calls 'Get' on Child using HttpClient).
I want all Child endpoints to require auth via Managed Identity and AAD, and I want all Parent endpoints to allow anonymous. However in Azure I want to set the Parent App Service to have permission to call the Child App Service. Therefore Child endpoints are only accessible by using Parent endpoints (or if you have permissions on a user account to directly use Child).
In the Azure Portal:
Authentication/Authorization
I have enabled 'App Service Authentication' on both App Services.
Child is set to 'Log in with AAD'.
Parent is set to 'Allow Anonymous requests'.
Both have AAD configured under 'Authentication Providers'.
Identity
Set to 'On' for both App Services
Access control (IAM)
Child has Parent as Role Assignment, Type = "App Service or Function App" and Role = "Contributer"
With all the above setup:
Calling Child -> Get, requires me to log in
Calling Parent -> Get, returns the expected response of 200 OK
Calling Parent -> GetChild, returns "401 - You do not have permission to view this directory or page"
Without the use of Client ids/Secrets/Keys/etc, as I thought the idea behind Managed Identity was to throw that all out the window, given all the above, should Parent be able to call Child? And if so, what have I setup wrong?
Calling Parent -> GetChild, returns "401 - You do not have permission to view this directory or page"
Without the use of Client ids/Secrets/Keys/etc, as I thought the idea
behind Managed Identity was to throw that all out the window, given
all the above, should Parent be able to call Child? And if so, what
have I setup wrong?
There are two things that I notice with current setup.
1. Acquire a token using Managed Identity to call "Child" service endpoint from "Parent"
Managed Identity only provides your app service with an identity (without the hassle of governing/maintaining application secrets or keys). This identiy can then be used to acquire tokens for different Azure Resources.
But it is still your App's responsibility to make use of this identity and acquire a token for relevant resource. In this case the relevant resource will be your "Child" API. I think this is probably the part you are missing right now.
Relevant documentation on Microsoft Docs - How to use managed identities for App Service and Azure Functions > Obtain tokens for Azure resources
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.KeyVault;
// ...
var azureServiceTokenProvider = new AzureServiceTokenProvider();
string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://vault.azure.net");
// change this to use identifierUri for your child app service.
// I have used the default value but in case you've used a different value, find it by going to Azure AD applications > your app registration > manifest
string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://<yourchildappservice>.azurewebsites.net");
This C#/.NET sample uses Microsoft.Azure.Services.AppAuthentication nuget package and acquires a token for Azure Key Vault. In your case, you will replace https://vault.azure.net with the identifierUri for your "Child" service. It's usually set to https://<yourappservicename>.azurewebsites.net by default, but you can find it's value by going to Azure AD applications and then finding the relevant app registration > manifest. You could also use applicationId for the target application (i.e. "Child") to acquire the token.
In case you're not using C#/.NET, same Microsoft Docs link above also has guidance on how to acuqire token using Managed Identity and REST based calls from any platform. Using REST Protocol
Here is a blog post that also gives a good walk through - Call Azure AD protected website using Managed Service Identity (MSI)
2. Azure RBAC Role Assignments are different from Azure AD roles that you may want to use
I see that you have assigned contributor role to Parent App Service's identity from IAM. This role assignment works for Azure RBAC and help in giving permissions for managing the resources, but Azure AD role claims work differently.
If what you were looking to do is to assign a role to parent app, which can be checked in child app and only then allow the calls there is a different way of setting this up.
I should first mention that this role based setup is for a little advanced scenario and not really mandatory to do. You should be able to call "Child" service from "Parent" once you follow the steps in point 1 described above.
Now once the call from Parent to Child is working, you may want to limit the access to Child app service to only "Parent" or a few valid applications. Here are two approaches to achieve that.
Both the approaches are explained on Microsoft Docs here - Microsoft identity platform and the OAuth 2.0 client credentials flow
Relate SO Posts and Blog
Is there a way to secure an Azure Function that will only be called from a specific Azure Logic App?
Azure Active Directory - How to restrict Backend API App Registration to a specific client App Registration
https://joonasw.net/view/calling-your-apis-with-aad-msi-using-app-permissions
Approach 1 - Use Access Control Lists
When your "Child" API receives a token, it can decode the token and extract the client's application ID from the appid and iss claims. Then it compares the application against an access control list (ACL) that it maintains.
Depending on your requirement, API might grant only a subset of full permissions or all permissions to a specific client.
Approach 2 - Use Application Permissions or Roles
Configure your child API application to expose a set of application permissions (or roles).
This approach is a little more declarative, as you define an application permission that needs to be assigned to any application that can call your child-api.
Navigate to Azure Active Directory > App Registrations > App registration for your child-api app > Manifest
Add a new application role.. using json like this:
"appRoles": [
{
"allowedMemberTypes": [
"Application"
],
"displayName": "Can invoke my API",
"id": "fc803414-3c61-4ebc-a5e5-cd1675c14bbb",
"isEnabled": true,
"description": "Apps that have this role have the ability to invoke my child API",
"value": "MyAPIValidClient"
}]
Assign the app permission to your frontend app
New-AzureADServiceAppRoleAssignment -ObjectId <parentApp.ObjectId> -PrincipalId <parentApp.ObjectId> -Id "fc803414-3c61-4ebc-a5e5-cd1675c14bbb" -ResourceId <childApp.ObjectId>
Now, in the auth token received by your child api, you can check that the role claims collection must contain a role named "MyAPIValidClient" otherwise you can reject the call with Unauthorized exception.
To expand on the accepted answer.
You need to define an "App Role" in the target app registration's manifest. This is the app registration which is used to represent the resource (API App Service).
Then you use the Azure CLI to grant permission for that "App Role" to the Enterprise App (The one generated when you setup a managed identity for the client app). See the "APIs and other Azure AD registered applications" in this article for detailed steps https://blog.yannickreekmans.be/secretless-applications-add-permissions-to-a-managed-identity/
You can retrieve the token using the following once the permissions have been granted. The code snippet below uses Azure.Identity which is now the recommended library for Managed Identity in Azure.
public class AzureAdTokenRetriever : IAzureAdTokenRetriever
{
private readonly ILogger<AzureAdTokenRetriever> logger;
private readonly IMemoryCache inMemoryCache;
public AzureAdTokenRetriever(
ILogger<AzureAdTokenRetriever> logger,
IMemoryCache inMemoryCache)
{
this.logger = logger;
this.inMemoryCache = inMemoryCache;
}
public async Task<string> GetTokenAsync(string resourceId, string scope = "/.default")
{
var resourceIdentifier = resourceId + scope;
if (inMemoryCache.TryGetValue(resourceIdentifier, out var token))
{
this.logger.LogDebug("Token for {ResourceId} and {Scope} were fetched from cache", resourceId, scope);
return (string)token;
}
var tokenCredential = new DefaultAzureCredential();
var accessToken = await tokenCredential.GetTokenAsync(
new TokenRequestContext(new [] { resourceIdentifier }), CancellationToken.None)
.ConfigureAwait(false);
// Set cache options with expiration 5 minutes before the token expires
var cacheEntryOptions = new MemoryCacheEntryOptions().SetAbsoluteExpiration(accessToken.ExpiresOn.AddMinutes(-5));
inMemoryCache.Set(resourceIdentifier, accessToken.Token, cacheEntryOptions);
this.logger.LogDebug("Token for {ResourceId} and {Scope} saved in cache with expiration of {TokenExpiry}",
resourceId, scope, cacheEntryOptions.AbsoluteExpiration);
return accessToken.Token;
}
}
I understand that Azure Functions are potentially open endpoints on the internet if I read Microsoft’s documentation correctly and per conversations with a friend who has some experience working with web development paradigms that Azure Functions leverages. A cursory reading of security forums and stack overflow questions on the topic leads me to understand at least a couple options of securing them namely
Azure Active Directory
Shared Access Signatures (SAS) and
Azure Virtual Networks.
Context/ What does my Azure Function do? It manages a blob container related to an ETL of vendor data from a SFTP source to a SQL Endpoint which this ETL utilizes an intermediary blob container for file transfer and long term cold storage of source data. The Azure Function moves the blobs from one container to an archive container after they have been loaded to the SQL endpoint. Why Azure Function to manage the blob containers?
SSIS lacks ability to perform blob manipulation (i.e copy and delete)
Logic App lacks ability to perform a join (of files loaded to SQL endpoint and file names in blob container)
An example of one of the functions is shown here below:
using System.IO;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Net.Http;
using System.Net;
using Microsoft.WindowsAzure.Storage.Blob;
using System.Collections.Generic;
using System.Text;
namespace AFA_ArchiveBlob
{
public static class HttpTrigger_BlobInput
{
[FunctionName("HttpTrigger_BlobInput")]
public static async Task<HttpResponseMessage> Run(
//public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "{name}")] HttpRequest req,
string name,
ILogger log,
[Blob("{name}/blobname",FileAccess.ReadWrite,Connection = "AzureWebJobsStorage")] CloudBlobContainer myCloudBlobContainer
)
{
//Execution Logged.
log.LogInformation($"HttpTrigger_BlobInput - C# HTTP trigger function processed a request.");
//Run the query against the blob to list the contents.
BlobContinuationToken continuationToken = null;
List<IListBlobItem> results = new List<IListBlobItem>();
do
{
var response = await myCloudBlobContainer.ListBlobsSegmentedAsync(continuationToken);
continuationToken = response.ContinuationToken;
results.AddRange(response.Results);
}
while (continuationToken != null);
//Query the names of the blobs. Todo: can this be a single line linq query select instead?
List<string> listBlobNames = new List<string>();
foreach (CloudBlockBlob b in results)
{
listBlobNames.Add(b.Name);
}
//Serialize the list of blob names to json for passing to function caller via return statement
var jsonReturn = JsonConvert.SerializeObject(listBlobNames);
log.LogInformation("Returning the following JSON");
log.LogInformation(jsonReturn);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(jsonReturn, Encoding.UTF8, "application/json")
};
}
}
}
Firstly, even though using keys might be convenient, I see that official documentation advises against using keys to secure function endpoint in production scenarios.
I suggest it would be a better choice to go with Azure Active Directory for security.. as explained here Secure an HTTP endpoint in production
How to Implement
I see two possible approaches:
1. Simple Approach: Check that calling application is your Azure logic app specifically
Enable Azure Active Directory Authentication for your Azure Function App. You can simply use Express settings (with create a new Azure AD app)
Enable Managed Service Identity for your Logic App.
Find out appid for Managed Service Identity associated with your logic app.. go to Azure Portal > Azure Active Directory > Enterprise Applications > All Applications > Relevant Service Principal (Explained in more detail with screenshots in another SO post here)
Authenticate your logic app to Azure function using Managed Service Identity as explained here.. Authenticate with managed identity in logic app.. note that resource being accessed will be your Azure function.
In your function code, now you can check that appid claim in access token should exactly match the appid for logic app (i.e. logic app is the one calling your function).. otherwise you can reject the call with Unauthorized exception.
2. A more declarative Approach: Have an application permission defined for Azure function app and check for this permission/role being present in auth token from client calling your function
This approach is a little more declarative, as you define an application permission that needs to be assigned to any application that can call your Azure function.
Enable Azure Active Directory Authentication for your Azure Function App. You can simply use Express settings (with create a new Azure AD app)
Now go to Azure Active Directory > App Registrations > App registration for your function app > Manifest
Add a new application role.. using json like this:
"appRoles": [
{
"allowedMemberTypes": [
"Application"
],
"displayName": "Can invoke my function",
"id": "fc803414-3c61-4ebc-a5e5-cd1675c14bbb",
"isEnabled": true,
"description": "Apps that have this role have the ability to invoke my Azure function",
"value": "MyFunctionValidClient"
}]
Enable Managed Service Identity for your Logic App.
Find out appid for Managed Service Identity associated with your logic app.. as already explained in approach 1 above
Assign the app permission to this managed service identity..
New-AzureADServiceAppRoleAssignment -ObjectId <logicappmsi.ObjectId> -PrincipalId <logicappmsi.ObjectId> -Id "fc803414-3c61-4ebc-a5e5-cd1675c14bbb" -ResourceId <yourfunctionaadapp.ObjectId>
Authenticate your logic app to Azure function using Managed Service Identity.. as already explained in approach 1 above
Now, in the auth token received by your function, you can check that the role claims collection must contain a role named "MyFunctionValidClient" otherwise you can reject the call with Unauthorized exception.
In addition to the above steps explained by #Rohit Below step is important:
Go to Host.json of the function.
Default authLevel : "function" should be changed to "authLevel": "anonymous".
This does not mean anyone can access the function as with Log on AD sign-in authentication required sign user however with managed identity in logic app function authenticate with service principle.
I have developed a .net console application which have these main characteristics :-
Integrate with SharePoint online REST API, to retrieve some list items, and modify the items fields.
Will run daily #1 am for example.
I will host this console application inside Azure Function app.
The Azure account does not have any permission on the sharepoint tenant, as the Azure account and the sharepoint online are on different domains.
so i am not sure under which account the console application will be running?
Will it runs under the current Azure account? if this is the case, then this will not work as the azure account is on different domain and does not have any permission on the sharepoint (and it shouldn't have)?
OR
I can define a service account for the Azure function app to run under it, where in this case i can define the service account to be an authorized account inside sharepoint online?
OR
i need to define the username/password inside the console application itself? i do not like to approach, as i will be exposing the password inside the console application. also changing the password for the username, means that we will need to update the console application accordingly..
so can anyone advice on this please?
Thanks
EDIT
code for managing the console application authentication :-
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint.Client;
namespace O365SPProject
{
class Program
{
private class Configuration
{
public static string ServiceSiteUrl = "https://<tenant>.sharepoint.com";
public static string ServiceUserName = "<user>#<tenant>.onmicrosoft.com";
public static string ServicePassword = "xxxxxxxxxx";
}
static ClientContext GetonlineContext()
{
var securePassword = new SecureString();
foreach (char c in Configuration.ServicePassword)
{
securePassword.AppendChar(c);
}
var onlineCredentials = new SharePointOnlineCredentials(Configuration.ServiceUserName, securePassword);
var context = new ClientContext(Configuration.ServiceSiteUrl);
context.Credentials = onlineCredentials;
return context;
}
static void Main(string[] args)
{
var ClientContext=GetonlineContext();
Web web = clientContext.Web;
// do somethings
}
}
}
There are multiple parts to your question, so I'll answer it accordingly.
1. Which option out of the 3 you mentioned (or if there is a different better option :)), should you use to manage your configuration data/service account identity
OPTION 4 (similar to your option 2 with subtle difference):
You should take your service account identity and configuration data out of your console application completely and pass them in through "Application Settings" for your Azure Function App.
This option is similar to the option 2 you had in your question, as you keep the information outside of console app code
I can define a service account for the Azure function app to run under
it, where in this case i can define the service account to be an
authorized account inside sharepoint online?
but difference is that I am not saying that you will be able to define a service account for your Azure function app to run under (because you can't control the account that Azure function will run under, Microsoft infrastructure takes care of it), instead you will pass it to your console app as a secure configuration data and your console app will use it. More on security/encryption later while comparing the options.
I actually took your console application code from question, created a console app and used it in a timer triggered Azure function to get it working. So these steps are from a working sample. I used the "Microsoft.SharePointOnline.CSOM" nuget package in my console app, and had to upload some of the dependency dlls along with exe in order for it to run. Feel free to ask for more details on doing this part if you run into issues.
Adding Application Settings - Navigate your Azure Function App and Click on "Application Settings"
Add Settings for all items that you want to take out of your console application and control from outside. I did it for all 3 items I saw, but this is up to you.
Then change your code to use these settings. I have shown the exact code changes at the end.
OPTION 5
Registering a new application in Azure AD to represent your Azure function.
You should register a new application in your Azure AD and use this identity to access SharePoint online.
You will need to grant permissions to SharePoint online for this application (NOTE: permission assignment will not be as granular or detailed as in case of your service account approach, I'll explain more while comparing the options)
You will need to associate a certificate with your AzureAD application to help in authentication.
While authenticating to SharePoint online, you will not be directly able to use the SharePointOnlineCredentials class as in your code today, but instead send the bearer token in 'Authorization' header for the http request.
Here is blog post that walks through detailed steps involved in this option 5.
NOTE: This blog still leaves out the certificate details like password in function code at the end, which will not be ideal and you will need to move it out to App Settings or Azure Key Vault ideally.
2. Which account will the .NET console application run under and a Quick Comparison of all options
It's an arbitrary IIS App Pool account, as pointed out by #Mitch Stewart, other SO posts and is evident in the output I get for my function, it's exact value in my run came out to be "IIS APPPOOL\mawsFnPlaceholder0_v1 ". See the image at the bottom. You already have some good info shared on this, so I'll not repeat. Only thing I'll add is that this account will be controlled by the infrastructure hosting your function app and will be designed more towards taking care of isolation/other concerns in a shared infrastructure where many function apps can run, so trying to control/change it may not be the way to go right now.
Option 1 (from your question) - Giving permissions to an IIS app pool account for your SharePoint Online site, especially when you don't control the account may not be a good idea.
Option 2 (from your question) - It would have been better than the other 2 options you mentioned, but you can't really control this account.
Option 3 (from your question)- Embedding this information deep into your console application will be a maintenance issue as well as not the most secure option unless you start reading form a vault etc. Maintenance issues will remain no matter what you do because it's embedded in compiled code, which it shouldn't be.
Option 4 - This is better than previous 3 options, because it separates the concern of code from configuration and identity information, no recompilation needed for updates. Also note that whatever you store in App Settings configurations is encrypted by default (with good governance of key rotation) and is the recommended way. These values are decrypted only just before execution of your app and loaded into process memory. Look detailed discussion in this link, I have also given a small relevant excerpt below -
Provide documentation about encrypt/decrypt settings
Even with this option you could store them in a key vault and then your setting would be the URL of the key vault secret that has the actual information.
Option 5 - This option makes use of Azure AD based identity to authenticate with SharePoint Online which is good part.
It does come with some additional effort and some limitations though, so you will need to consider if these limitations are acceptable or not in your scenario:
Permissions for SharePoint online will not be as granular/detailed as a user being given permissions from inside SharePoint Users/Groups interfaces (no site/list/folder/item level specific permissions etc). In this approach, you will give the permissions as part of setting up Azure AD application and you will only get generic options like these (shown in screenshot below)
Microsoft has some well documented limitations in this scenario, which you can read here: What are the limitations when using app-only
So overall, I would suggest you choose option 4 or option 5, or a combination of both for your implementation depending on which limitations are acceptable in your scenario.
3. Code Changes to use App Settings
Just the important Change
public static string ServiceSiteUrl = Environment.GetEnvironmentVariable("ServiceSiteUrl");
public static string ServiceUserName = Environment.GetEnvironmentVariable("ServiceUserName");
public static string ServicePassword = Environment.GetEnvironmentVariable("ServicePassword");
Full Code in a working Sample (I replaced do something with reading the title and Url for SharePoint Web object):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint.Client;
using System.Security;
using System.Security.Principal;
namespace O365SPProject
{
class Program
{
private class Configuration
{
public static string ServiceSiteUrl = Environment.GetEnvironmentVariable("ServiceSiteUrl");
public static string ServiceUserName = Environment.GetEnvironmentVariable("ServiceUserName");
public static string ServicePassword = Environment.GetEnvironmentVariable("ServicePassword");
}
static ClientContext GetonlineContext()
{
var securePassword = new SecureString();
foreach (char c in Configuration.ServicePassword)
{
securePassword.AppendChar(c);
}
var onlineCredentials = new SharePointOnlineCredentials(Configuration.ServiceUserName, securePassword);
var context = new ClientContext(Configuration.ServiceSiteUrl);
context.Credentials = onlineCredentials;
return context;
}
static void Main(string[] args)
{
var ClientContext = GetonlineContext();
ClientContext.Load(ClientContext.Web);
ClientContext.ExecuteQuery();
Console.WriteLine("This app found web title as: {0} and URL as: {1}",
ClientContext.Web.Title, ClientContext.Web.Url);
Console.WriteLine("Console app is running with identity {0}", WindowsIdentity.GetCurrent().Name);
}
}
}
OUTPUT on executing Azure Function
The SharePoint REST API supports OAuth. Here's a promising article. Although, this might be a bit much for you intentions. Alternatively, you can try using basic auth (username + password). To guard against plain text passwords, you can store them in Azure Key Vault.
Edit
The current user of an Azure function is the identity of the IIS app pool.