Using a managed service identity to call into SharePoint Online. Possible? - azure-active-directory

I have been playing around with Managed Service Identity in Azure Logic Apps and Azure Function Apps. I think it is the best thing since sliced bread and am trying to enable various scenarios, one of which is using the MSI to get an app-only token and call into SharePoint Online.
Using Logic Apps, I generated a managed service identity for my app, and granted it Sites.readwrite.All on the SharePoint application. When then using the HTTP action I was able to call REST endpoints while using Managed Service Identity as Authentication and using https://.sharepoint.com as the audience.
I then though I'd take it a step further and create a function app and follow the same pattern. I created the app, generated the MSI, added it the Sites.readwrite.All role same way I did with the Logic App.
I then used the code below to retrieve an access token and try and generate a clientcontext:
#r "Newtonsoft.Json"
using Newtonsoft.Json;
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.SharePoint.Client;
public static void Run(string input, TraceWriter log)
{
string resource = "https://<tenant>.sharepoint.com";
string apiversion = "2017-09-01";
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Secret", Environment.GetEnvironmentVariable("MSI_SECRET"));
var response = client.GetAsync(String.Format("{0}/?resource={1}&api-version={2}", Environment.GetEnvironmentVariable("MSI_ENDPOINT"), resource, apiversion)).Result;
var responseContent = response.Content;
string responseString = responseContent.ReadAsStringAsync().Result.ToString();
var json = JsonConvert.DeserializeObject<dynamic>(responseString);
string accesstoken = json.access_token.ToString()
ClientContext ctx = new ClientContext("<siteurl>");
ctx.AuthenticationMode = ClientAuthenticationMode.Anonymous;
ctx.FormDigestHandlingEnabled = false;
ctx.ExecutingWebRequest += delegate (object sender, WebRequestEventArgs e){
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accesstoken;
};
Web web = ctx.Web;
ctx.Load(web);
ctx.ExecuteQuery();
log.Info(web.Id.ToString());
}
}
The bearer token is generated, but requests fail with a 401 access denied (reason="There has been an error authenticating the request.";category="invalid_client")
I have tried to change the audience to 00000003-0000-0ff1-ce00-000000000000/.sharepoint.com#" but that gives a different 401 error, basically stating it cannot validate the audience uri. ("error_description":"Exception of type 'Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException' was thrown.). I have also replace the CSOM call with a REST call mimicking the same call I did using the Logic App.
My understanding of oauth 2 is not good enough to understand why I'm running into an issue and where to look next.
Why is the Logic App call using the HTTP action working, and why is the Function App not working??
Anyone?

Pretty basic question: Have you tried putting an '/' at the end of the resource string e.g. https://[tenant].sharepoint.com/ ?
This issue is also present in other endpoints so it may be that its interfering here as well.

Related

ASP.NET 6 WebAPI Authentication with SSO

I have an ASP.NET 6.0 Web API project. I would like to add authentication and authorization to it, but it must use SSO via Azure.
We already have a SPA application that does this, it uses the Angular MSAL library to redirect the user to an SSO Login page, then returns to the SPA with an access token. The access token is then added to the header of each request to the Web API, which uses it to enforce authentication.
Now we want to share our web API with other teams within our organization, and we would like to have that login process just be another API call, rather than a web page.
Conceptually, a client would hit the /login endpoint of our API, passing in a userID and password. The web API would then get an access token from Azure, then return it as the payload of the login request. It's then up to the client to add that token to subsequent request headers.
I have done this with regular ASP.NET Identity, where all of the user and role data is stored in a SQL database, but since our organization uses SSO via Azure Active Directory, we would rather use that.
I have researched this topic online, and so far all of the examples I have seen use a separate SPA, just like we already have. But as this is a web api, not a front-end, we need to have an API method that does this instead.
Is this even possible? I know Microsoft would rather not have user credentials flow through our own web server, where a dishonest programmer might store them for later misuse. I understand that. But I'm not sure there's a way around this.
Thanks.
I believe you are looking for the Resource Owner Password (ROP) flow. You can use IdentityModel.OidcClient to implement it.
Sample code:
public class Program
{
static async Task Main()
{
// call this in your /login endpoint and return the access token to the client
var response = await RequestTokenAsync("bob", "bob");
if (!response.IsError)
{
var accessToken = response.AccessToken;
Console.WriteLine(accessToken);
}
}
static async Task<TokenResponse> RequestTokenAsync(string userName, string password)
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync(Constants.Authority);
if (disco.IsError) throw new Exception(disco.Error);
var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "roclient",
ClientSecret = "secret",
UserName = userName,
Password = password,
Scope = "resource1.scope1 resource2.scope1",
Parameters =
{
{ "acr_values", "tenant:custom_account_store1 foo bar quux" }
}
});
if (response.IsError) throw new Exception(response.Error);
return response;
}
}
Sample taken from IdentityServer4 repository where you can find more ROP flow client examples.
I would recommend that you don't go with this implementation and instead have all clients obtain their access tokens directly from Azure AD like you did with your Angular SPA.

How to get session id in static method while making callout

I am working on something which includes LWC with tooling API. I wrote this below method which makes a callout. but when I call this method this method from lwc at that time I'm unable to get session Id, but if I call this same method from the developer console then it works fine.
Apex Code:
#AuraEnabled
public static string getList(String fieldName){
HttpRequest req = new HttpRequest();
req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
System.debug('res------>'+UserInfo.getSessionID());
req.setHeader('Content-Type', 'application/json');
req.setEndpoint('callout:Tooling_Query/query/?q=Select+id,Namespaceprefix,developername,TableEnumOrId+FROM+customfield+Where+developername+LIKE\'' +fieldName+ '\'');
req.setMethod('GET');
Http h = new Http();
HttpResponse res = h.send(req);
System.debug('res------>'+res.getBody());
return res.getBody();
}
When I call it from lwc it returns this
[{"message":"This session is not valid for use with the REST API","errorCode":"INVALID_SESSION_ID"}]
so, how can I get session-id from lwc, I already set up a Connected App and Named Credential by the name of Tooling_Query
and add URL to remote sites.
please help me here.
You can't. Your Apex code called in a Lightning Web Components context cannot get an API-enabled Session Id. This is documented in the Lightning Web Components Dev Guide:
By security policy, sessions created by Lightning components aren’t enabled for API access. This restriction prevents even your Apex code from making API calls to Salesforce. Using a named credential for specific API calls allows you to carefully and selectively bypass this security restriction.
The restrictions on API-enabled sessions aren’t accidental. Carefully review any code that uses a named credential to ensure you’re not creating a vulnerability.
The only supported approach is to use a Named Credential authenticated as a specific user.
There is a hack floating around that exploits a Visualforce page to obtain a Session Id from such an Apex context. I do not recommend doing this, especially if you need to access the privileged Tooling API. Use the correct solution and build a Named Credential.

Azure Authentication from client app to another API

I'm trying to get azure AD authentication working between a Blazor WASM app, and another API that I have running locally but on a different port. I need both applications to use the Azure login, but I only want the user to have to log in once on the Blazor app which should then pass those credentials through to the API.
I've set up app registrations for both apps in the portal, created the redirect url, exposed the API with a scope and I can successfully log into the blazor app and see my name using #context.User.Identity.Name.
When it then tries to call the API though, I get a 401 error back and it doesn't hit any breakpoints in the API (presumably because there is no authentication being passed across in the http request).
My code in the Blazor app sets up a http client with the base address set to the API:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddHttpClient("APIClient", client => client.BaseAddress = new Uri("https://localhost:11001"))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("APIClient"));
builder.Services.AddMsalAuthentication<RemoteAuthenticationState, CustomUserAccount>(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://d3152e51-9f5e-4ff7-85f2-8df5df5e2b2e/MyAPI");
//options.UserOptions.RoleClaim = "appRole";
});
await builder.Build().RunAsync();
}
In my API, I just have the Authorise attribute set on the class, and eventually will need roles in there too:
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class CarController
Then, in my Blazor component, I then inject the http factory and try to make a request:
#inject IHttpClientFactory _factory
...
private async Task RetrieveCars()
{
var httpClient = _factory.CreateClient("APIClient");
HttpResponseMessage response = await httpClient.GetAsync("https://localhost:11001/api/cars");
var resp = await response.Content.ReadAsStringAsync();
cars = JsonSerializer.Deserialize<List<Car>>(resp);
}
but this returns the 401 error. I've also tried a few different variations like just injecting a http client (#inject HttpClient Http) but nothing seems to be adding my authorisation into the API calls. The options.UserOptions.RoleClaim is also commented out in the AddMsalAuthentication section as I wasn't sure if it was needed, but it doesn't work with or without it in there.
Can anyone explain what I'm doing wrong and what code I should be using?
Common causes.
Most cases ,we tend to forget to grant consent after giving API
permissions in the app registration portal,after exposing the api
which may lead to unauthorized error.
Other thing is when Audience doesn’t match the “aud” claim when we
track the token in jwt.io .Make sure ,Audience=clientId is configured
in the code in authentication scheme or Token validation parameters
by giving ValidAudiences.And also try with and without api:// prefix
in client id parameter.
Sometimes aud claim doesn’t match as we mistakenly send ID token
instead of Access tokens as access tokens are meant to call APIs .So
make sure you check mark both ID Token and access token in portal
while app registration.
While Enabling the authentication by injecting the [Authorize]
attribute to the Razor pages.Also add reference
Microsoft.AspNetCore.Authorization as(#using
Microsoft.AspNetCore.Authorization)
Please see the note in MS docs and some common-errors
If above are not the cases, please provide with additional error details and startup configurations or any link that you are following to investigate further.

Azure B2C authentication without a ClientSecret

I've successfully implemented this tutorial and have a client + server working locally.
However, the front-end application that I'm building is an Angular app - this means that it isn't possible to store a client secret in it..
Relevant code:
ConfidentialClientApplication cca = new ConfidentialClientApplication(Startup.ClientId, Startup.Authority, Startup.RedirectUri, new ClientCredential(Startup.ClientSecret), userTokenCache, null);
var user = cca.Users.FirstOrDefault();
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scope, user, Startup.Authority, false);
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, apiEndpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
How can I set my frontend up securely to work without having a client secret, based on the tutorial mentioned?
I'm currently using this angular library .
You can achieve this by using implicit grant flow
You can seamlessly integrate into any SPA application.
Follow https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-reference-spa, this article helps you.
There is an SPA sample already available in GitHub, you can try https://github.com/Azure-Samples/active-directory-b2c-javascript-hellojs-singlepageapp
you can use oidc-client library instead hello.js in above sample, both are very similar and easy to implement.

OpenIdConnectAuthenticationNotifications.AuthorizationCodeReceived event not firing

using this sample:
https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-openidconnect
This works as expected when running it locally
But when we deploy it (azure web app), it still authenticates, but the OpenIdConnectAuthenticationNotifications.AuthorizationCodeReceived event is not firing.
This the code.
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
PostLogoutRedirectUri = redirectUri,
RedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed
}
});
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));
Uri uri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path));
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(code, uri, credential, graphResourceId);
}
This is a problem because it requires caching of the token to make an outbound call.
Since it doesn’t have it, it throws.
There was an issue that caused this related to a trailing slash after the redir url but we’ve already tried that.
So two questions…
1) Under what conditions would event get fired and why would this work when running locally? According to the docs it should be "Invoked after security token validation if an authorization code is present in the protocol message."
2) What is the best way to debug this? Not clear on what to look for here.
1) Under what conditions would event get fired and why would this work when running locally? According to the docs it should be "Invoked after security token validation if an authorization code is present in the protocol message."
As the document point, this event will fire when the web app verify the authorization code is present in the protocol message.
2) What is the best way to debug this? Not clear on what to look for here.
There are many reason may cause the exception when you call the request with the access_token. For example, based on the code you were using the NaiveSessionCache which persist the token using the Sesstion object. It means that you may also get the exception when you deploy the web app with multiple instance. To trouble shoot this issue, I suggest that you remote debug the project to find the root cause. For remote debug, you can refer the document below:
Introduction to Remote Debugging on Azure Web Sites

Resources