Blog Post
Superfast using Web App and Managed Identity to invoke Function App triggers
Great post! Thanks a lot ChunHan. For anyone wondering what are the $IDENTITY_ENDPOINT and $IDENTITY_HEADER, they are predefined by MS in a web app runtime, https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=portal%2Chttp#rest-endpoint-reference.
One thing I don't like much about this built-in setup is that it brings this extra setup of an App Registration to represent your APIs in function app and that default scope "api://9c0012ad-XXXX" that does not make much sense or even matter to a developer when I just want very basic authentication. Not to say they are useless but just in my case it's extra effort.
If you happen to be builting an frontend, already have an App Registration for your frontend website and you don't want complicated authorization, I have below setup to validate the JWT token (ID token) acquired when user authenticate into your frontend app. for your reference. In JavaScript/TypeScript, I use higher order function. But in python, you can implement it with decorator.
async function someDumbTask(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {}
const handler = withIDTokenValidation(someDumbTask);
app.http('aDumbTask', {
methods: ['POST'],
authLevel: 'anonymous',
handler: handler
});
// ======= Implementation of withIDTokenValidation =======
import { HttpHandler, HttpRequest, InvocationContext } from "@azure/functions";
import jwt from 'jsonwebtoken';
export function withIDTokenValidation(
handler: HttpHandler
): HttpHandler {
return async (request: HttpRequest, context: InvocationContext) => {
try {
const IDToken = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!IDToken) {
throw new Error("ID Token is required for validation.");
}
// Parse JWT header to get kid
const [headerB64] = IDToken.split('.');
const header = JSON.parse(Buffer.from(headerB64, 'base64').toString('utf-8'));
const kid = header.kid;
if (!kid) throw new Error('No kid found in token header');
// Fetch public keys from Entra ID
const response = await fetch(`https://login.microsoftonline.com/common/discovery/keys`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Failed to fetch keys: ${response.statusText}`);
}
const keys = await response.json();
const key = keys.keys.find((k: any) => k.kid === kid);
if (!key) throw new Error('No matching key found for kid');
// Build public key
const publicKey = `-----BEGIN CERTIFICATE-----\n${key.x5c[0]}\n-----END CERTIFICATE-----`;
const expectedAudience = "xx-xx-xx-xx-xx"
const expectedTenantId = "xx-xx-xx-xx-xx";
// Verify token with audience check and get decoded payload
const decoded = jwt.verify(IDToken, publicKey, { algorithms: ['RS256'], audience: expectedAudience });
// Check tid (tenant ID) claim
// decoded can be string or object, but for JWTs it's always an object
if (typeof decoded !== 'object' || !decoded.tid) {
throw new Error('No tid claim found in token payload');
}
if (decoded.tid !== expectedTenantId) {
throw new Error('Token tid claim does not match expected tenant ID');
}
return handler(request, context);
} catch (error) {
console.error("Token validation failed:", error);
return {
status: 401,
body: 'Unauthorized: Invalid or expired ID Token.'
};
}
};
}
thank you for your input! it truly sparked further insights.