Azure Functions with JWT Authentication
If you’re building Azure Functions, you generally have two options when it comes to implementing authentication and authorization:
- Use the App Service Authentication integration which is great if you are using one of the standard identity providers (Azure AD, Microsoft Account, Facebook, Google, and Twitter).
- Use custom authentication. The Microsoft documentation discusses this in the context of a standard MVC app and leaves some gaps.
Let’s take a look at how we can implement custom authentication for Azure Functions using JWT.
(I’ve pushed a fully functioning example here so you have a quick start to work from: https://github.com/CharlieDigital/AzFunctionsJwtAuth to get your own authentication scenario working)
Setting Up Our Project
It’s important to note that when you use custom authentication with Functions, you want to set up your application with anonymous authentication as we will be handling authentication checks at the function level.
Performing the Credential Check
The first step is to perform the credential check. This simply means we check to see if some set of credentials we’ve received matches a set of credentials stored in a custom database or some other service/protocol other than the standard ones provided by App Services. In this example, we are going to pass a JSON object as the argument to our function and simply return true each time.
Here is our simple stub in Functions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/// <summary> /// Service class for performing authentication. /// </summary> public class AuthenticationService { [FunctionName("Authenticate")] public async Task<IActionResult> Authenticate( // https://stackoverflow.com/a/52748884/116051 [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth")] Credentials credentials, ILogger log) { bool authenticated = true; // We're just returning a static true for this example // [ TODO: PERFORM YOUR CUSTOM AUTHENTICATION HERE; HOWEVER YOU LIKE ] if (!authenticated) { return new UnauthorizedResult(); } else { return new OkObjectResult($"Authenticated {credentials.User}!"); } } } /// <summary> /// DTO for transferring the auth info. /// </summary> public class Credentials { public string User { get; set; } public string Password { get; set; } } |
In the real world, you would implement your custom logic for checking whether the user’s authentication credentials were valid or not. You can use any mechanism you like such as an existing database, WebAuthn, an Excel spreadsheet (hey, if that’s what floats your boat!)
We can test this service in Postman:
Issuing the JWT
JSON Web Tokens are simply a mechanism for securely transferring JSON strings. Rather than having the user pass credentials back and forth on each call and then checking the user’s credentials over and over, we can simply retrieve the relevant information about the user and encrypt it into a token which we return to the client as a cookie. When the client makes subsequent calls, the client passes the JWT back which the application will decrypt and verify that the contents are valid.
Note that because the token is passed with each request, you’ll want to be cautious not to go overboard and only add key data like the username, user ID, user email, role(s) and so on.
We’re going to use the JWT package to generate and parse our tokens:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
/// <summary> /// Service class for performing authentication. /// </summary> public class AuthenticationService { private readonly IJwtAlgorithm _algorithm; private readonly IJsonSerializer _serializer; private readonly IBase64UrlEncoder _base64Encoder; private readonly IJwtEncoder _jwtEncoder; public AuthenticationService() { // JWT specific initialization. // https://github.com/jwt-dotnet/jwt _algorithm = new HMACSHA256Algorithm(); _serializer = new JsonNetSerializer(); _base64Encoder = new JwtBase64UrlEncoder(); _jwtEncoder = new JwtEncoder(_algorithm, _serializer, _base64Encoder); } [FunctionName("Authenticate")] public async Task<IActionResult> Authenticate( // https://stackoverflow.com/a/52748884/116051 [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth")] Credentials credentials, ILogger log) { // TODO: Perform custom authentication here; we're just using a simple hard coded check for this example bool authenticated = credentials?.User.Equals("charles", StringComparison.InvariantCultureIgnoreCase) ?? false; if (!authenticated) { return new UnauthorizedResult(); } // Instead of returning a string, we'll return the JWT with a set of claims about the user Dictionary<string, object> claims = new Dictionary<string, object> { // JSON representation of the user Reference with ID and display name { "username", credentials.User }, // TODO: Add other claims here as necessary; maybe from a user database { "role", "admin"} }; string token = _jwtEncoder.Encode(claims, "YOUR_SECRETY_KEY_JUST_A_LONG_STRING"); // Put this key in config return new OkObjectResult(token); } } |
Now when we call the function, we get back a JWT token which is encrypted with our key (which is just an arbitrary string; pick a long, random one!):
In the application, you’ll want to hold onto this and then return it with each request.
For example, in Axios, we can do something like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
axios.interceptors.request.use( (config) => { // Vuex store or any other front-end storage depending on your app const token = store.state.app.authToken; if (token) { config.headers['Authorization'] = `Bearer ${token}`; } return config; } ); |
This ensures that each subsequent function invocation through Axios will automatically have the authorization token included as a header.
Consuming the JWT
Now that we have the token, let’s see how we can consume it.
We’re going to write a simple wrapper class which encapsulates the decoding portion of the JWT flow:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
/// <summary> /// Wrapper class for encapsulating claims parsing. /// </summary> public class AuthenticationInfo { public bool IsValid { get; } public string Username { get; } public string Role { get; } public AuthenticationInfo(HttpRequest request) { // Check if we have a header. if (!request.Headers.ContainsKey("Authorization")) { IsValid = false; return; } string authorizationHeader = request.Headers["Authorization"]; // Check if the value is empty. if (string.IsNullOrEmpty(authorizationHeader)) { IsValid = false; return; } // Check if we can decode the header. IDictionary<string, object> claims = null; try { if (authorizationHeader.StartsWith("Bearer")) { authorizationHeader = authorizationHeader.Substring(7); } // Validate the token and decode the claims. claims = new JwtBuilder() .WithAlgorithm(new HMACSHA256Algorithm()) .WithSecret(AuthenticationService.SECRET_KEY) .MustVerifySignature() .Decode<IDictionary<string, object>>(authorizationHeader); } catch(Exception exception) { IsValid = false; return; } // Check if we have user claim. if (!claims.ContainsKey("username")) { IsValid = false; return; } IsValid = true; Username = Convert.ToString(claims["username"]); Role = Convert.ToString(claims["role"]); } } |
And here’s how we use it from a hypothetical function that updates a user password:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[FunctionName("ChangePassword")] public async Task<IActionResult> ChangePassword( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "changepassword")] HttpRequest req, // Note: we need the underlying request to get the header ILogger log) { // Check if we have authentication info. AuthenticationInfo auth = new AuthenticationInfo(req); if (!auth.IsValid) { return new UnauthorizedResult(); // No authentication info. } string newPassword = await req.ReadAsStringAsync(); return new OkObjectResult($"{auth.Username} changed password to {newPassword}"); } |
We need the request this time as it is necessary for reading the header.
When we make the request from Postman, we need to add the Authorization header manually by copying the result from the authorization call:
And we set the body to “5678”.
That’s all there is to it!
Next Steps
There are a few challenges with this approach. Namely, it means that you need to remember to perform an authorization check with each function invocation. It is possible to use IFunctionInvocationFilter to build a generic base class that checks authorization for each call and then makes the claims available on the base class. This simplifies implementation of authenticated and authorized services.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
/// <summary> /// Base class for authenticated service which checks the incoming JWT token. /// </summary> public abstract class AuthorizedServiceBase : IFunctionInvocationFilter { private const string AuthenticationHeaderName = "Authorization"; // Access the authentication info. protected AuthenticationInfo Auth { get; private set; } /// <summary> /// Pre-execution filter. /// </summary> /// <remarks> /// This mechanism can be used to extract the authentication information. /// </remarks> public Task OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancellationToken) { HttpRequest message = executingContext.Arguments.First().Value as HttpRequest; if (message == null || !message.Headers.ContainsKey(AuthenticationHeaderName)) { return Task.FromException(new AuthenticationException("No Authorization header was present")); } try { Auth = new AuthenticationInfo(message); } catch (Exception exception) { return Task.FromException(exception); } if (!Auth.IsValid) { return Task.FromException(new KeyNotFoundException("No identity key was found in the claims.")); } return Task.CompletedTask; } /// <summary> /// Post-execution filter. /// </summary> public Task OnExecutedAsync(FunctionExecutedContext executedContext, CancellationToken cancellationToken) { // Nothing. return Task.CompletedTask; } } |
Now we can simply inherit from this class and all of our functions within our new class will be authorized!
Grab the source code here: https://github.com/CharlieDigital/AzFunctionsJwtAuth
If you liked this article, you might also be interested in Azure Functions with CosmosDB and GraphQL.
3 Responses
[…] Check out my followup article on how to perform custom authentication and authorization in Functions… […]
[…] For service level authentication and authorization, check out my other article on Azure Functions and JWT. […]
[…] I’ve encountered my fair share of gaps in the Azure documentation (one of the reasons I wrote this post on Functions with JWT authentication), but I know that the AWS documentation — at least as it pertains to Amplify and AppSync […]