-
Notifications
You must be signed in to change notification settings - Fork 0
Authentication and authorization
1.1.0 to 1.2.0 change replaced the HttpJwtAuthorizeAttribute with HttpAuthorizeAttribute
which now can handle multiple different authentication schemes. In addition, options also changed
accordingly. The breaking change was made intentionally without deprecation notice
due to not having officially published the library at that time, as the impact to users was miniscule.
AzureFunctionsV2.HttpExtensions provides an attribute based authentication and
authorization feature that can be enabled by configuring the authentication filter
and by applying the HttpAuthorizeAttribute to the action method. Different schemes
of authentication are supported:
- Basic auth
- ApiKey based auth, either in a header or in a query parameter
- OAuth2 based auth
- JWT based auth
The implementation
itself is rather simple: if a method has the HttpAuthorizeAttribute
(or any attribute inheriting from it) applied and the
authentication/authorization has been configured properly, the authentication checks
will be applied.
As implied above, you may also create your own attribute that inherits from the
HttpAuthorizeAttribute that provides more utility than the default attribute, and
then use the attribute properties in your custom authorization implementation, as
your implementation gets these attributes as a parameter.
Example usage:
[HttpAuthorize(Scheme.HeaderApiKey)]
[FunctionName("webhook")]
public static async Task<IActionResult> MyWebhook(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "webhook")] HttpRequest req,
ILogger log)
{
return new OkObjectResult(new MyResponse() { Message = $"Webhook call ok" });
}For basic auth the procedure is simple: we check for username/password combinations, which can be configured in your startup code. This example pretty much explains it completely:
[HttpAuthorize(Scheme.Basic)]
[FunctionName("secrets")]
public static async Task<IActionResult> MyWebhook(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "secrets")] HttpRequest req,
ILogger log)
{
return new OkObjectResult(new MyResponse() { Message = $"This call is protected, and you are authorized!" });
}using System.Collections.Generic;
using System.Security.Claims;
using AzureFunctionsV2.HttpExtensions.Authorization;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Extensions.DependencyInjection;
using NSwag.SwaggerGeneration.AzureFunctionsV2.Tests.HttpExtensionsApp.Startup;
[assembly: WebJobsStartup(typeof(Startup), "MyStartup")]
namespace NSwag.SwaggerGeneration.AzureFunctionsV2.Tests.HttpExtensionsApp.Startup
{
public class Startup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
builder.Services.Configure<HttpAuthenticationOptions>(options =>
{
options.BasicAuthentication = new BasicAuthenticationParameters()
{
ValidCredentials = new Dictionary<string, string>() { { "user", "pass" } }
};
});
}
}
}Basically all you need to do is supply the valid credentials for the HttpAuthenticationOptions.BasicAuthentication and you're good to go.
The ApiKey authentication is also very simple to configure, except that the verification is performed through an action that either returns true or false. This allows a little bit more flexibility regarding API key testing.
API key can be conveyed either via a query parameter or a header.
Again, an example that shows the usage and configuration:
[HttpAuthorize(Scheme.HeaderApiKey)]
[FunctionName("webhook")]
public static async Task<IActionResult> MyWebhook(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "webhook")] HttpRequest req,
ILogger log)
{
return new OkObjectResult(new MyResponse() { Message = $"Webhook call ok" });
}using System.Collections.Generic;
using System.Security.Claims;
using AzureFunctionsV2.HttpExtensions.Authorization;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Extensions.DependencyInjection;
using NSwag.SwaggerGeneration.AzureFunctionsV2.Tests.HttpExtensionsApp.Startup;
[assembly: WebJobsStartup(typeof(Startup), "MyStartup")]
namespace NSwag.SwaggerGeneration.AzureFunctionsV2.Tests.HttpExtensionsApp.Startup
{
public class Startup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
builder.Services.Configure<HttpAuthenticationOptions>(options =>
{
options.ApiKeyAuthentication = new ApiKeyAuthenticationParameters()
{
ApiKeyVerifier = async (s, request) => s == "key" ? true : false,
HeaderName = "x-apikey"
};
});
}
}
}OAuth2 authentication relies heavily on your custom implementation of authentication/authorization. Since the token validation and generating claims depends completely on how you decide to implement it (ie. the token can be anything), much of this work falls on you.
The procedure is simple: your custom authorization filter has to process the token
and the request and return a ClaimsPrincipal of the user (which will then be
assigned to the HttpUser type parameter if one exists in the Function signature),
or throw an exception if the authentication/authorization fails.
Example showing the usage and configuration:
[HttpAuthorize(Scheme.OAuth2)]
[FunctionName("userdata")]
public static async Task<IActionResult> UserData(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "userdata")] HttpRequest req,
[HttpToken]HttpUser user,
ILogger log)
{
return new OkObjectResult(new MyUser() {Id = user.ClaimsPrincipal.Identity.Name, NickName = user.ClaimsPrincipal.Claims.First(x => x.Type == "nickname").Value});
}using System.Collections.Generic;
using System.Security.Claims;
using AzureFunctionsV2.HttpExtensions.Authorization;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Extensions.DependencyInjection;
using NSwag.SwaggerGeneration.AzureFunctionsV2.Tests.HttpExtensionsApp.Startup;
[assembly: WebJobsStartup(typeof(Startup), "MyStartup")]
namespace NSwag.SwaggerGeneration.AzureFunctionsV2.Tests.HttpExtensionsApp.Startup
{
public class Startup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
builder.Services.Configure<HttpAuthenticationOptions>(options =>
{
options.OAuth2Authentication = new OAuth2AuthenticationParameters()
{
CustomAuthorizationFilter = (token, request, attributes) =>
{
// Parse token, authorize, return ClaimsPrincipal or throw an exception if auth fails
throw new HttpAuthenticationException("Unauthorized");
}
};
});
}
}
}The JWT authentication simplifies the task of validating a JWT token. All you need to do
is provide the validation parameters, and after that all JWT authorized Functions will
always perform JWT validation. The HttpAuthorizationFilter will check for an
Authorization header with a Bearer token and will attempt to validate the token. If the
validation fails an exception gets thrown (and the default exception filter will return 401).
If a custom authorization filter has been defined in the JwtAuthenticationOptions it will
be called after the token has been validated to perform further authorization checks.
If the token is valid and custom authorization code does not throw an exception, the user
is considered authorized.
To start using JWT auth in your Function App, you'll need to configure the JwtAuthenticationOptions
in your startup code.
The options have two properties:
public class JwtAuthenticationOptions
{
public TokenValidationParameters TokenValidationParameters { get; set; }
public Func<ClaimsPrincipal, SecurityToken, IList<HttpJwtAuthorizeAttribute>, Task> CustomAuthorizationFilter { get; set; }
}The TokenValidationParameters define how to validate the token. You can use the default
TokenValidationParameters class for this or alternatively use the provided
OpenIdConnectJwtValidationParameters class in case you're using an OIDC endpoint
like in the case of using Auth0.
The CustomAuthorizationFilter is (if provided) used to authorize the user after the token has been validated. The resolved ClaimsPrincipal, SecurityToken and the list of HttpJwtAuthorizeAttributes applied to the method are provided as parameters to the custom validator.
Configuration using an OIDC endpoint (Auth0):
using System.Collections.Generic;
using System.Security.Claims;
using AzureFunctionsV2.HttpExtensions.Authorization;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Extensions.DependencyInjection;
using NSwag.SwaggerGeneration.AzureFunctionsV2.Tests.HttpExtensionsApp.Startup;
[assembly: WebJobsStartup(typeof(Startup), "MyStartup")]
namespace NSwag.SwaggerGeneration.AzureFunctionsV2.Tests.HttpExtensionsApp.Startup
{
public class Startup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
builder.Services.Configure<HttpAuthenticationOptions>(options =>
{
options.JwtAuthentication = new JwtAuthenticationParameters()
{
TokenValidationParameters = new OpenIdConnectJwtValidationParameters()
{
OpenIdConnectConfigurationUrl =
"https://jusas-tests.eu.auth0.com/.well-known/openid-configuration",
ValidAudiences = new List<string>()
{"XLjNBiBCx3_CZUAK3gagLSC_PPQjBDzB"},
ValidateIssuerSigningKey = true,
NameClaimType = ClaimTypes.NameIdentifier
},
AuthorizationFilter = async (principal, token, attributes) => { }
};
});
}
}
}Configuration with a manually provided security key.
builder.Services.Configure<JwtAuthenticationOptions>(options =>
{
string publicCert = @"my-base64-encoded-certificate";
var x509cert = new X509Certificate2(Convert.FromBase64String(publicCert));
SecurityKey sk = new X509SecurityKey(x509cert);
sk.KeyId = x509cert.Thumbprint;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuers = new List<string>() { "https://my-issuer" },
ValidAudiences = new List<string>() { "my-audience" },
IssuerSigningKeys = new List<SecurityKey>() { sk },
ValidateIssuerSigningKey = true,
NameClaimType = ClaimTypes.NameIdentifier
};
});When using OAuth2 or Jwt authentication schemes for your Functions,
the populated ClaimsPrincipal will be available in a HttpUser type
parameter if one is present in the Function signature:
public static async Task<IActionResult> MyFunction(..., [HttpToken]HttpUser user)So to gain access to the resolved ClaimsPrincipal inside your Function code, you
should add the a parameter of type HttpUser to your Function signature.
You must also apply the HttpToken Binding attribute to it; this indicates that this
parameter is populated from the Bearer token and is also internally used
to identify the HttpUser parameter and is technically necessary in order to be able to have
the parameter present in the Function signature.
[FunctionName("JwtAuthTest1")]
[HttpAuthorize(Scheme.Jwt)] // Require JWT authorization.
public static async Task<IActionResult> JwtAuthTest1(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
[HttpToken]HttpUser user, // ClaimsPrincipal will be populated with user data here.
ILogger log)
{
// user.ClaimsPrincipal now contains the ClaimsPrincipal.
}// Define your own attribute.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class MyOwnAuthorizeAttribute : HttpAuthorizeAttribute
{
public string RequiredRole { get; set; }
}
// The Function.
[FunctionName("JwtAuthTest1")]
[MyOwnAuthorize(RequiredRole = "admin")] // Require JWT authorization.
public static async Task<IActionResult> JwtAuthTest1(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
[HttpToken]HttpUser user, // ClaimsPrincipal will be populated with user data here.
ILogger log)
{
}
// Custom authorization code inside startup.
builder.Services.Configure<HttpAuthenticationOptions>(options =>
{
// ...
// Optional authorization code.
options.JwtAuthentication = new JwtAuthenticationParameters()
{
CustomAuthorizationFilter = async (principal, token, attributes) =>
{
MyOwnAuthorizeAttribute myAttr = attributes.FirstOrDefault(a => a.GetType() == typeof(MyOwnAuthorize));
if(myAttr != null)
{
// Get role from UserRepository or user's ClaimsPrincipal or something and compare...
if(userRole != myAttr.RequiredRole)
throw new HttpUnauthorizedException("User does not have the required role!");
}
};
}
});Table of contents
- Overview
- Authentication and authorization
- Exception handling
- HttpParam Attributes
-
Classes and interfaces overview
- Implementable interfaces
- Infrastructure classes