I have a dotnet 5 Azure Function (dotnet-isolated) that is triggered by an HTTP call.
The function will be called by a different Azure function app and I'd like to secure the target with Azure AD and use "client-credentations" OAuth2 flow.
I found an excellent sample that informs for my scenario at: Microsoft GitHub Sample
My problem is, the sample uses a WebApi app as the service. This has access to the HttpContext object uses an extension method in the Microsft.Identity.Web assembly called "ValidateAppRole"
My Azure function does have a parameter of type HttpRequestData. This has a headers property containing key value pairs. One of those keys is called "Authorization" and its value is the Access Token provided by Azure AD. I've run this token through jwt.ms and can confirm that the "Roles" collection contains the custom role I need to validate. So I know the information required is present; I just don't know how to check for it programmatically.
Since a dotnet-isolated Azure function doesn't seem to have access to the HttpContext object. How can a check equivalent to the following be made?
HttpContext.ValidateAppRole("CustomRoleName");
I've made a sample Functions app for this: https://github.com/juunas11/IsolatedFunctionsAuthentication.
The sample includes two middleware: AuthenticationMiddleware and AuthorizationMiddleware. The former validates the JWT and creates a ClaimsPrincipal from it, while the latter checks for claims on the ClaimsPrincipal based on attributes set on the Function method.
You can see the full code in the GitHub repository.
The authentication middleware sets the ClaimsPrincipal to the FunctionContext
with:
context.Features.Set(new JwtPrincipalFeature(principal, token));
The authorization middleware can then get it from the same context object with:
var principalFeature = context.Features.Get<JwtPrincipalFeature>();
The code can then check for specific claims.
The same context object is also available within the Function methods. In the sample, attributes are used with middleware so that the Function code itself does not have to check for authorization.
Juunas gave an excellent baseline for using middleware to do this, for which I am very grateful!
I have updated the token authentication middleware to leverage the new features available in .Net 7. I hope this helps others looking for some guidance!
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Middleware;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker.Http;
using System.Security.Claims;
namespace IdentityAccessTestIsolated.Middleware
{
public class TokenAuthenticationMiddleware : IFunctionsWorkerMiddleware
{
private readonly JwtSecurityTokenHandler _tokenValidator;
private readonly TokenValidationParameters _tokenValidationParameters;
private readonly ConfigurationManager<OpenIdConnectConfiguration> _configurationManager;
private readonly string _tenantId;
public TokenAuthenticationMiddleware(IConfiguration configuration)
{
_tenantId = configuration["TenantId"];
var audience = configuration["AuthenticationClientId"];
_tokenValidator = new JwtSecurityTokenHandler();
_tokenValidationParameters = new TokenValidationParameters
{
ValidAudience = audience
};
_configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
$"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever());
}
public async Task Invoke(
FunctionContext context,
FunctionExecutionDelegate next)
{
var req = await context.GetHttpRequestDataAsync();
if (!TryGetTokenFromHeaders(req, out var token))
{
// No token from headers
var res = req!.CreateResponse(HttpStatusCode.Unauthorized);
await res.WriteStringAsync("The supplied token was invalid");
context.GetInvocationResult().Value = res;
return;
}
if (!_tokenValidator.CanReadToken(token))
{
// Token is malformed
var res = req!.CreateResponse(HttpStatusCode.Unauthorized);
await res.WriteStringAsync("The supplied token was invalid");
context.GetInvocationResult().Value = res;
return;
}
// Get OpenID Connect metadata
var validationParameters = _tokenValidationParameters.Clone();
var openIdConfig = await _configurationManager.GetConfigurationAsync(default);
validationParameters.ValidIssuer = openIdConfig.Issuer;
validationParameters.ValidIssuer = openIdConfig.Issuer.Replace("{tenantid}", _tenantId); // manually modify the string to include our tenant
validationParameters.IssuerSigningKeys = openIdConfig.SigningKeys;
try
{
// Validate token
var principal = _tokenValidator.ValidateToken(
token, validationParameters, out _);
// Set principal + token in Features collection
// They can be accessed from here later in the call chain
context.Features.Set<ClaimsPrincipal>(principal);
await next(context);
}
catch (Exception e)
{
// Token is not valid (expired etc.)
var res = req!.CreateResponse(HttpStatusCode.Unauthorized);
await res.WriteStringAsync("The supplied token was invalid");
context.GetInvocationResult().Value = res;
return;
}
}
private static bool TryGetTokenFromHeaders(Microsoft.Azure.Functions.Worker.Http.HttpRequestData req, out string token)
{
token = null;
// HTTP headers are in the binding context as a JSON object
// The first checks ensure that we have the JSON string
var headers = req.Headers;
var authHeader = req.Headers.FirstOrDefault(a => a.Key.ToLowerInvariant() == "authorization").Value.FirstOrDefault();
if (authHeader is null) return false;
if (!authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
// Scheme is not Bearer
return false;
}
token = authHeader.Substring("Bearer ".Length).Trim();
return true;
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With