How to use a token authentification from Graph API in Azure http trigger function as webhook - authentication

I used this authentication to handle Graph api calls in a webhook function.
I tried other authentication samples but this was the only only one which worked with the credentials for a service principal (app permission are needed, not user permissions - so some other auth samples doesn't worked for me)
CallRecords.Read.All - Anwendung - Read all call records - Ja - Gewährt für
The credentials are saved in a keyvault.
But the problem is on massive number of calls the authentication fails
(15.000 -20.000 request/day)
IConfidentialClientApplication app;
AuthenticationResult result = null;
string[] scopes = null;
string data = null;
IRestResponse response = null;
// build Authenticate for getting a token
config.Authority += config.CallRecordTenantID;
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithClientSecret(config.ClientSecret)
.WithAuthority(new Uri(config.Authority))
.Build();
scopes = new string[] { "https://graph.microsoft.com/.default" };
try
{
result = await app.AcquireTokenForClient(scopes)
.ExecuteAsync();
}
catch (Exception ex)
{
log.LogInformation("Exc.:" + ex.Message + "\n" + ex.StackTrace);
}
After getting many request near same time the authentication crashes; how can I avoid that?
2022-01-31T06:51:59Z [Information] AADSTS900023: Specified tenant identifier 'a1ae89fb-21b9-40bf-9d82-a10ae85a2407a1ae89fb-21b9-40bf-9d82-a10ae85a2407' is neither a valid DNS name, nor a valid external domain.
Trace ID: ebbb51ed-2ef1-4931-81b6-702833c93f00
Correlation ID: cb112afc-08bf-44bb-9a8e-b0a93938f6cb
Timestamp: 2022-01-31 06:51:55Z
at Microsoft.Identity.Client.Internal.Requests.RequestBase.HandleTokenRefreshError(MsalServiceException e, MsalAccessTokenCacheItem cachedAccessTokenItem)
at Microsoft.Identity.Client.Internal.Requests.ClientCredentialRequest.ExecuteAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.ApiConfig.Executors.ConfidentialClientExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenForClientParameters clientParameters, CancellationToken cancellationToken)
at Namespace1.Webhook.GetOnlyDesiredDataAfterFiltersAsync(String CallRecordId, AzFuncCallRecordsConfiguration config, ILogger log) in D:\Repos\Webhook.cs:line 518
at Namespace1.Webhook.Run(HttpRequest req, ExecutionContext context, ILogger log) in D:\Repos..\BWebhook.cs:line 180
So is there is no better way, for the app service (Azure function in function app) to use authentication token.
Or is there a better way to use other webhook e.g. durable functions with orchestration with the entities?
Or is there an easier solution to bind bind the service principal to the function app direct, so the auth token must not requested in code?
Or other ideas?
I hope someone can can give advice what I can do.
Thanks

Related

AzureAD, Client confidential app calling webapi with a custom Application ID URI, returns 401

I'm trying to develop an API which can be called from different web apps.
If I call the api with a client confidential app, using the default scope (api://[APIclientId]/.default), everything works.
But If I specify a custom Application ID URI for the API app registration (like: api://myapi.iss.it), and I set the scope to api://myapi.iss.it/.default, I get HTTP401 from the webapp.
This is the method to retrieve the token for the webapp to call the api:
private async Task PrepareAuthenticatedClient()
{
IConfidentialClientApplication app;
string AURY = String.Format(CultureInfo.InvariantCulture, _config["AzureAd:Instance"] + "{0}", _config["AzureAd:TenantId"]);
app = ConfidentialClientApplicationBuilder.Create(_config["AzureAd:ClientId"])
.WithClientSecret(_config["AzureAd:ClientSecret"])
.WithAuthority(new Uri(AURY))
.Build();
var accessToken = await app.AcquireTokenForClient(new string[] { _config["API:scope"] }).ExecuteAsync();
Console.WriteLine("token: " + accessToken.AccessToken);
//var accessToken = await _tokenAcquisition.GetAccessTokenForAppAsync(_TodoListScope);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken);
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
I notice that the Audience is still api://[APIclientId] in the token, even if I set the api:scope to api://myapi.iss.it/.default
Is it correct?
any idea what could be the problem?
I got the solution on the Microsoft Q&A platform.
Basically, I didn't specifiy the Audience in the API application, so by default it was "api://[APIclientId]".
When the API was verifing the token of the app (where the aud was api://myapi.iss.it), the exception "Microsoft.IdentityModel.Tokens.SecurityTokenInvalidAudienceException" was raised, and the API returned 401.
If you have the same problem, and you are using the Microsoft.Indentity.Web library, specifying the Audience in the appsetting.json may be enough.

Which is the correct flow to get current user's groups from Microsoft graph?

Hi I am implementing Groups based authorization to my web api. I have client application swagger. Through swagger I am logging in and calling web api. In web api I want to implement groups based authorization through Microsoft graph. When I logging through swagger I will get one token and I am passing to my webapi. If I am not wrong, Now I required one token to call Microsoft graph. So can I use same token to call microsoft graph? I confused my self and implemented client credential flow. Client credential flow will get token for the app(here user signed in token has nothing to do).
public static async Task<GraphServiceClient> GetGraphServiceClient()
{
// Get Access Token and Microsoft Graph Client using access token and microsoft graph v1.0 endpoint
var delegateAuthProvider = await GetAuthProvider();
// Initializing the GraphServiceClient
graphClient = new GraphServiceClient(graphAPIEndpoint, delegateAuthProvider);
return graphClient;
}
private static async Task<IAuthenticationProvider> GetAuthProvider()
{
AuthenticationContext authenticationContext = new AuthenticationContext(authority);
ClientCredential clientCred = new ClientCredential(clientId, clientSecret);
// ADAL includes an in memory cache, so this call will only send a message to the server if the cached token is expired.
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenAsync(graphResource, clientCred).ConfigureAwait(false);
var token = authenticationResult.AccessToken;
var delegateAuthProvider = new DelegateAuthenticationProvider((requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token.ToString());
return Task.FromResult(0);
});
return delegateAuthProvider;
}
Below code will return all the groups.
GraphServiceClient client = await MicrosoftGraphClient.GetGraphServiceClient();
var groupList = await client.Groups.Request().GetAsync();
but my requirement is to get current signed in users group. So can someone help me which flow I should use and In the above code only Is it possible to get current users group? Can someone help me in understanding these and implement correctly? Any help would be greatly appreciated. Thanks
As we have discussed before, you should call Microsoft Graph API from your webapi app.
So you should not use the same access token to call Microsoft Graph. You should specfy the Microsoft Graph endpoint (https://graph.microsoft.com) as the resource when you request a new access token to Microsoft Graph.
Secondly, client credential flow means app-only permission (without user). So if there is no signed in user, how could we get user's groups?
You should consider using AcquireTokenAsync(String, ClientAssertion, UserAssertion).
After that, using the following code to get the signed in user's groups.
GraphServiceClient client = await MicrosoftGraphClient.GetGraphServiceClient();
var memberOf = await graphClient.Me.MemberOf.Request().GetAsync();

How to add additional claims for MVC client with IdentityServer4

I'm using the IdentityServer4 "AspNetCoreAndApis" sample application found here
It has a token server and an MVC client application.
The identity server project has an external OIDC authentication provider set up using their demo server - https://demo.identityserver.io/
After hitting a protected endpoint in MvcClient, being redirected to the local identity server, choosing and authenticating with the demo server, it reaches the ExternalController callback of the local identity server. At this point I would like to issue additional claims to the user, and have them be available in MvcClient.
There's code in the callback to addadditionalLocalClaims and issue a cookie. I tried adding another claim:
var additionalLocalClaims = new List<Claim>();
additionalLocalClaims.Add(new Claim("TestKey", "TestValue"));
await HttpContext.SignInAsync(user.SubjectId, user.Username, provider, localSignInProps, additionalLocalClaims.ToArray());
But by the time the user arrives in the HomeController of MvcClient this claim is not there.
I think I don't properly understand which authentication scheme is being used where, and the function of the relevant cookies.
EDIT:
In response to the first comment below, I tried attaching a claim to a requested scope, but still no luck - this is the in memory resource store:
public static IEnumerable<ApiResource> Apis
{
get
{
var apiResource = new ApiResource("api1", "My API");
apiResource.UserClaims.Add("TestKey");
var resources = new List<ApiResource>
{
apiResource
};
return resources;
}
}
The MvcClient is both allowed the api1 scope, and requests it.
Your client MVC could get the user's custom claims from ID token or UserInfo endpoint .
To add claims to ID token , you can set client's config :AlwaysIncludeUserClaimsInIdToken . But involve all user claims in ID token is not recommended concern about the size of ID Token .
A better solution is making your client app get user's claims from UserInfo endpoint :
public class MyProfileService : IProfileService
{
public MyProfileService()
{ }
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var claims = new List<Claim>()
{
new Claim("TestKey", "TestValue")
};
context.IssuedClaims.AddRange(claims);
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
// await base.IsActiveAsync(context);
return Task.CompletedTask;
}
}
Register in DI :
services.AddTransient<IProfileService, MyProfileService>();
The IProfileService service could be used to add claims to ID Token, Access token and UserInfo endpoint . By default the custom claims won't involve in ID Token event using IProfileService , the reason explained above - the ID token size . So you can make your client app get claims from UserInfo endpoint with OIDC middleware config :
options.Scope.Add("profile");
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapJsonKey("TestKey", "TestKey");
Above codes will add OIDC profile permission to get claims from endpoint , and send a request to connect/userinfo endpoint with ID Token , and get claims and map claim whose name is TestKey to your client's claim principle and save to cookie . Now you can get the claims with User.Claims in MVC .

Not able to SignOut using Saml2 from Sustainsys

This should be redirecting my app to my AdFs signOut Page, and then redirect me back to my app.
However, it simply redirects me to my route "/logout".
Watching the log on my ADFS server nothing happens.
[AllowAnonymous]
[HttpGet]
[Route("api/logout")]
public async Task<IActionResult> Logout()
{
return SignOut(new AuthenticationProperties()
{
RedirectUri = "/logout"
},
Saml2Defaults.Scheme);
}
SignIn works fine. I even tried this same approach, but does not work. Here, the ReturnUrl method gets the location from HttpContext.Response.Header. When I try this for the logout, the location is always null.
[AllowAnonymous]
[HttpGet]
[Route("api/login")]
public async Task<string> LoginAdfs()
{
string redirectUri = _appSettings.Saml.SpEntityId;
await HttpContext.ChallengeAsync(new AuthenticationProperties
{
RedirectUri = string.Concat(redirectUri, "/autenticado")
});
return ReturnUrl();
}
Any idea what could be happening?
UPDATE 21/11/2019
Turns out the Saml2Handler is simply not trying to send the request to the server. I'm getting these messages on my output window:
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Debug: Initiating logout, checking requirements for federated logout
Issuer of LogoutNameIdentifier claim (should be Idp entity id):
Issuer is a known Idp: False
Session index claim (should have a value):
Idp has SingleLogoutServiceUrl:
There is a signingCertificate in SPOptions: True
Idp configured to DisableOutboundLogoutRequests (should be false):
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Information: Federated logout not possible, redirecting to post-logout
Here is my StartUp Configuration, I don't get what is wrong here:
ServiceCertificate se = new ServiceCertificate()
{
Certificate = new X509Certificate2(SpCert, "",X509KeyStorageFlags.MachineKeySet),
Use = CertificateUse.Signing
};
SPOptions sp = new SPOptions
{
AuthenticateRequestSigningBehavior = SigningBehavior.Never,
EntityId = new EntityId(SpEntityId),
ReturnUrl = new Uri("/login"),
NameIdPolicy = new Sustainsys.Saml2.Saml2P.Saml2NameIdPolicy(null, Sustainsys.Saml2.Saml2P.NameIdFormat.Unspecified),
};
sp.ServiceCertificates.Add(se);
IdentityProvider idp = new IdentityProvider(new EntityId(appSettings.Saml.EntityId), sp);
idp.Binding = Saml2BindingType.HttpPost;
idp.AllowUnsolicitedAuthnResponse = true;
//idp.WantAuthnRequestsSigned = true;
idp.SingleSignOnServiceUrl = new Uri("/login");
//idp.LoadMetadata = true;
idp.SigningKeys.AddConfiguredKey(new X509Certificate2(IdpCert));
idp.MetadataLocation = theMetadata;
idp.DisableOutboundLogoutRequests = true;
For the logout to work, two special claims "LogoutNameIdentifier" and "SessionIndex" (full names are http://Sustainsys.se/Saml2/LogoutNameIdentifier and http://Sustainsys.se/Saml2/SessionIndex need to be present on the user. Those carries information about the current session that the Saml2 library needs to be able to do a logout.
Now I don't see your entire Startup, so I cannot understand your application's flow. But those claims should be present in the identity returned by the library - possibly stored in an External cookie (if you are using asp.net identity). When your application then sets the application cookie those two claims must be carried over to the session identity.
Also you have actually disabled outbound logout with DisableOutboundLogoutRequests. But that's not the main problem here as your logs indicates that the required claims are not present.
From my own experience, the two claims, as mentioned by Anders Abel, should be present on the user. I had not seen these claims until I passed all of the claims along with the sign-in request. ASP.NET Core recreates the principal on SignInAsync and needs claims to be passed in with the request.
With the following, I am able to fulfill a SingleLogout with my service:
await HttpContext.SignInAsync(user.SubjectId, user.Username, props, user.Claims.ToArray());
what you are using as a service provider.

Where to hook into WCF Pipeline to extract credentials for UserNamePasswordValidator from incoming HTTP Request Headers

Can anyone point me to a suitable WCF Extension Point for hooking into the WCF Pipeline to extract credentials for UserNamePasswordValidator from the headers of an incoming HTTP REST Request?
Yes I know about all the funky stunts with Http Handlers etc. you can pull to somehow get Basic/Digest Auth working but since the client I'm working on will be strictly Javascript based I've opted for a simple model where the credentials are passed using two custom headers over an SSL pipe.
Update: I've managed to improve on this by using the approach described here. While this does not solves the problem described in my question, it gets rid of having to authenticate in a authorization policy since authentication is now handled by a custom AuthenticationManager, bypassing the UsernamePasswordValidator alltogether.
For the time being I've solved the problem by combining Authentication and Authorization in a custom Authorization Policy. I'd still rather find a way to hook into the normal UserNamePasswordValidator authentication scheme because an Authorization Policy is supposed to to Authorization not Authentication.
internal class RESTAuthorizationPolicy : IAuthorizationPolicy
{
public RESTAuthorizationPolicy()
{
Id = Guid.NewGuid().ToString();
Issuer = ClaimSet.System;
}
public bool Evaluate(EvaluationContext evaluationContext, ref object state)
{
const String HttpRequestKey = "httpRequest";
const String UsernameHeaderKey = "x-ms-credentials-username";
const String PasswordHeaderKey = "x-ms-credentials-password";
const String IdentitiesKey = "Identities";
const String PrincipalKey = "Principal";
// Check if the properties of the context has the identities list
if (evaluationContext.Properties.Count > 0 ||
evaluationContext.Properties.ContainsKey(IdentitiesKey) ||
!OperationContext.Current.IncomingMessageProperties.ContainsKey(HttpRequestKey))
return false;
// get http request
var httpRequest = (HttpRequestMessageProperty)OperationContext.Current.IncomingMessageProperties[HttpRequestKey];
// extract credentials
var username = httpRequest.Headers[UsernameHeaderKey];
var password = httpRequest.Headers[PasswordHeaderKey];
// verify credentials complete
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return false;
// Get or create the identities list
if (!evaluationContext.Properties.ContainsKey(IdentitiesKey))
evaluationContext.Properties[IdentitiesKey] = new List<IIdentity>();
var identities = (List<IIdentity>) evaluationContext.Properties[IdentitiesKey];
// lookup user
using (var con = ServiceLocator.Current.GetInstance<IDbConnection>())
{
using (var userDao = ServiceLocator.Current.GetDao<IUserDao>(con))
{
var user = userDao.GetUserByUsernamePassword(username, password);
...