Skip to content
This repository was archived by the owner on Jun 3, 2023. It is now read-only.

Authentication and authorization

Jussi Saarivirta edited this page Feb 16, 2019 · 2 revisions

Authentication and authorization

Note: Breaking change notice

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.

Overview

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" });
}

Basic authentication

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.

ApiKey authentication

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

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");
                    }
                };
            });
        }
    }
}

JWT authentication

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.

Example configurations

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
  };
});

The HttpToken attribute and HttpUser class

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.

Examples

Example basic usage

[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.
}

Example advanced usage

// 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!");
            }
        };
    }
});

Clone this wiki locally