Azure security baseline for Azure Functions well describes the security consideration in general while developing an Azure Functions application. In addition to that, Azure Functions offers a built-in authentication method through the functions key. If you use the OpenAPI extension for Azure Functions, you can define the endpoint authentication and authorisation for each API endpoint in various ways. You can even try them through the Swagger UI page. Throughout this post, I'm going to discuss six different approaches for access control to Azure Functions API endpoints using the OpenAPI extension.
This GitHub repository contains the sample app used in this post.
OpenAPI Spec for Authentication
It could be a good idea to take a look at the authentication spec defined in OpenAPI before going further.
type
: defines what type of authentication method will be used. Currently, it acceptsAPI Key
,HTTP
,OAuth2
, andOpenID Connect
. But, the OpenAPI v2 spec doesn't support the OpenID Connect.name
: declares the auth key name. It's required for API Key.in
: defines the location of the auth key. It's required for API Key and acceptsquery
,header
, orcookie
.scheme
: declares the auth scheme. It's required for HTTP auth and accepts eitherBasic
orBearer
.bearerFormat
: usesJWT
in most cases when using theBearer
token through the HTTP auth.flows
: is required for theOAuth2
auth. Its value can beimplicit
,password
,clientCredentials
, orauthorizationCode
.openIdConnectUrl
: is necessary for theOpenID Connect
auth. However, it is advised to use eitherOAuth2
orBearer
auth for the OpenAPI v2 spec.
Based on the understandings above, let's apply the different auth approach to Azure Function endpoints through the OpenAPI extension.
APK Key in Querystring
This is the built-in feature of Azure Functions. Let's take a look at the code below. If you installed the OpenAPI extension, you could add the decorators. Spot on the OpenApiSecurityAttribute(...)
decorator, which sets the value (line #6-9).
Type
:SecuritySchemeType.ApiKey
In
:OpenApiSecurityLocationType.Query
Name
:code
public static class ApiKeyInQueryAuthFlowHttpTrigger | |
{ | |
[FunctionName(nameof(ApiKeyInQueryAuthFlowHttpTrigger))] | |
[OpenApiOperation(operationId: "apikey.query", tags: new[] { "apikey" }, Summary = "API Key authentication code flow via querystring", Description = "This shows the API Key authentication code flow via querystring", Visibility = OpenApiVisibilityType.Important)] | |
[OpenApiSecurity("apikeyquery_auth", | |
SecuritySchemeType.ApiKey, | |
In = OpenApiSecurityLocationType.Query, | |
Name = "code")] | |
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(Dictionary<string, string>), Summary = "successful operation", Description = "successful operation")] | |
public static async Task<IActionResult> Run( | |
[HttpTrigger(AuthorizationLevel.Function, "GET", Route = null)] HttpRequest req, | |
ILogger log) | |
{ | |
log.LogInformation("C# HTTP trigger function processed a request."); | |
var queries = req.Query.ToDictionary(q => q.Key, q => (string) q.Value); | |
var result = new OkObjectResult(queries); | |
return await Task.FromResult(result).ConfigureAwait(false); | |
} | |
} |
Run the function app, and you will see the Swagger UI page.
Click the lock button on the right-hand side to enter the API key value. This value will be appended to the querystring parameter.
The result screen shows the API key passed through the querystring parameter, code
.
API Key in Request Header
It's also the Azure Function's built-in feature. This time, set the value of the OpenApiSecurityAttribute(...)
decorator like below (line #6-9).
Type
:SecuritySchemeType.ApiKey
In
:OpenApiSecurityLocationType.Header
Name
:x-functions-key
public static class ApiKeyInHeaderAuthFlowHttpTrigger | |
{ | |
[FunctionName(nameof(ApiKeyInHeaderAuthFlowHttpTrigger))] | |
[OpenApiOperation(operationId: "apikey.header", tags: new[] { "apikey" }, Summary = "API Key authentication code flow via header", Description = "This shows the API Key authentication code flow via header", Visibility = OpenApiVisibilityType.Important)] | |
[OpenApiSecurity("apikeyheader_auth", | |
SecuritySchemeType.ApiKey, | |
In = OpenApiSecurityLocationType.Header, | |
Name = "x-functions-key")] | |
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(Dictionary<string, string>), Summary = "successful operation", Description = "successful operation")] | |
public static async Task<IActionResult> Run( | |
[HttpTrigger(AuthorizationLevel.Function, "GET", Route = null)] HttpRequest req, | |
ILogger log) | |
{ | |
log.LogInformation("C# HTTP trigger function processed a request."); | |
var headers = req.Headers.ToDictionary(q => q.Key, q => (string) q.Value); | |
var result = new OkObjectResult(headers); | |
return await Task.FromResult(result).ConfigureAwait(false); | |
} | |
} |
Run the function app and see the Swagger UI page.
If you want to authenticate the endpoint, enter the API key value to the field, labelled as x-functions-key
.
As a result, the API key was sent through the request header, x-functions-key
.
Basic Auth Token
Let's use the Basic auth token this time. Set the property values of OpenApiSecurityAttribute(...)
(line #6-8).
Type
:SecuritySchemeType.Http
Scheme
:OpenApiSecuritySchemeType.Basic
As this is not the built-in feature, you can use this approach for additional auth methods or replace the built-in feature. If you don't want to use the built-in API key, you should set the auth level value of the HttpTrigger
binding to AuthorizationLevel.Anonymous
(line #12).
public static class HttpBasicAuthFlowHttpTrigger | |
{ | |
[FunctionName(nameof(HttpBasicAuthFlowHttpTrigger))] | |
[OpenApiOperation(operationId: "http.basic", tags: new[] { "http" }, Summary = "Basic authentication token flow via header", Description = "This shows the basic authentication token flow via header", Visibility = OpenApiVisibilityType.Important)] | |
[OpenApiSecurity("basic_auth", | |
SecuritySchemeType.Http, | |
Scheme = OpenApiSecuritySchemeType.Basic)] | |
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(Dictionary<string, string>), Summary = "successful operation", Description = "successful operation")] | |
public static async Task<IActionResult> Run( | |
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = null)] HttpRequest req, | |
ILogger log) | |
{ | |
log.LogInformation("C# HTTP trigger function processed a request."); | |
var headers = req.Headers.ToDictionary(q => q.Key, q => (string) q.Value); | |
var result = new OkObjectResult(headers); | |
return await Task.FromResult(result).ConfigureAwait(false); | |
} | |
} |
Run the app to see the Swagger UI like below.
To authenticate your endpoint, you should enter the Username and Password, added to the Authorization
header.
The result screen shows the request header of Authorization
with the base64 encoded value.
Then, you should validate the auth details with your custom logic.
Bearer Auth Token
Similarly, this time, let's use the Bearer auth token. Set the property values of OpenApiSecurityAttribute(...)
(line #5).
Type
:SecuritySchemeType.Http
Scheme
:OpenApiSecuritySchemeType.Bearer
BearerFormat
:JWT
You now know how to set the auth level of the HttpTrigger
binding to AuthorizationLevel.Anonymous
(line #13).
public static class HttpBearerAuthFlowHttpTrigger | |
{ | |
[FunctionName(nameof(HttpBearerAuthFlowHttpTrigger))] | |
[OpenApiOperation(operationId: "http.bearer", tags: new[] { "http" }, Summary = "Bearer authentication token flow via header", Description = "This shows the bearer authentication token flow via header", Visibility = OpenApiVisibilityType.Important)] | |
[OpenApiSecurity("bearer_auth", | |
SecuritySchemeType.Http, | |
Scheme = OpenApiSecuritySchemeType.Bearer, | |
BearerFormat = "JWT")] | |
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(Dictionary<string, string>), Summary = "successful operation", Description = "successful operation")] | |
public static async Task<IActionResult> Run( | |
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = null)] HttpRequest req, | |
ILogger log) | |
{ | |
log.LogInformation("C# HTTP trigger function processed a request."); | |
var headers = req.Headers.ToDictionary(q => q.Key, q => (string) q.Value); | |
var handler = new JwtSecurityTokenHandler(); | |
var token = handler.ReadJwtToken(headers["Authorization"].Split(' ').Last()); | |
var claims = token.Claims.Select(p => p.ToString()); | |
var content = new { headers = headers, claims = claims }; | |
var result = new OkObjectResult(content); | |
return await Task.FromResult(result).ConfigureAwait(false); | |
} | |
} |
Run the function app and see the Swagger UI page.
During the authentication, you are asked to enter the Bearer token value. The Authorization
header will add the value.
The result screen shows the JWT value in the Authorization
header.
You should decode the JWT and find the appropriate claims and validate them for further processing.
OAuth2 Implicit Auth Flow
Although there are many ways in the OAuth2 authentication flow, I'm going to use the Implicit flow for this time. Set the properties of OpenApiSecurityAttribute(...)
(line #6-8).
Type
:SecuritySchemeType.OAuth2
Flows
:ImplicitAuthFlow
Auth level is also set to Anonymous
(line #12).
public static class OAuthImplicitAuthFlowHttpTrigger | |
{ | |
[FunctionName(nameof(OAuthImplicitAuthFlowHttpTrigger))] | |
[OpenApiOperation(operationId: "oauth.flows.implicit", tags: new[] { "oauth" }, Summary = "OAuth implicit flows", Description = "This shows the OAuth implicit flows", Visibility = OpenApiVisibilityType.Important)] | |
[OpenApiSecurity("implicit_auth", | |
SecuritySchemeType.OAuth2, | |
Flows = typeof(ImplicitAuthFlow))] | |
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(IEnumerable<string>), Summary = "successful operation", Description = "successful operation")] | |
public static async Task<IActionResult> Run( | |
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = null)] HttpRequest req, | |
ILogger log) | |
{ | |
log.LogInformation("C# HTTP trigger function processed a request."); | |
var headers = req.Headers.ToDictionary(p => p.Key, p => (string) p.Value); | |
var handler = new JwtSecurityTokenHandler(); | |
var token = handler.ReadJwtToken(headers["Authorization"].Split(' ').Last()); | |
var claims = token.Claims.Select(p => p.ToString()); | |
var result = new OkObjectResult(claims); | |
return await Task.FromResult(result).ConfigureAwait(false); | |
} | |
} |
You can see ImplicitAuthFlow
as the flow type. Since it uses Azure Active Directory, it sets AuthorizationUrl
, RefreshUrl
, and Scopes
values. It also takes the single tenant type, which requires the tenant ID (line #3-6, 10, 14-15). Scopes
has the default value (line #17).
public class ImplicitAuthFlow : OpenApiOAuthSecurityFlows | |
{ | |
private const string AuthorisationUrl = | |
"https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize"; | |
private const string RefreshUrl = | |
"https://login.microsoftonline.com/{0}/oauth2/v2.0/token"; | |
public ImplicitAuthFlow() | |
{ | |
var tenantId = Environment.GetEnvironmentVariable("OpenApi__Auth__TenantId"); | |
this.Implicit = new OpenApiOAuthFlow() | |
{ | |
AuthorizationUrl = new Uri(string.Format(AuthorisationUrl, tenantId)), | |
RefreshUrl = new Uri(string.Format(RefreshUrl, tenantId)), | |
Scopes = { { "https://graph.microsoft.com/.default", "Default scope defined in the app" } } | |
}; | |
} | |
} |
Run the function app and check the Swagger UI page.
When you click the lock button, it asks you to enter the client ID value, redirecting you to sign in to Azure Active Directory. Then, you will get the access token.
The result shows the Authorization
header with the access token in the JWT format.
That JWT is now decoded and verified for further processing.
OpenID Connect Auth Flow
Finally, let's use the OpenID Connect auth flow. OpenApiSecurityAttribute(...)
contains the following definitions (line #6-9).
Type
:SecuritySchemeType.OpenIdConnect
OpenIdConnectUrl
:https://login.microsoftonline.com/{tenant_id}/v2.0/.well-known/openid-configuration
OpenIdConnectScopes
:openid,profile
The {tenant_id}
value, of course, should be replaced with the real tenant ID. With this OpenID Connect URL, it automatically discovers the OAuth2 auth flows. Then, set the auth level to Anonymous
(line #12).
public static class OpenIDConnectAuthFlowHttpTrigger | |
{ | |
[FunctionName(nameof(OpenIDConnectAuthFlowHttpTrigger))] | |
[OpenApiOperation(operationId: "openidconnect", tags: new[] { "oidc" }, Summary = "OpenID Connect auth flows", Description = "This shows the OpenID Connect auth flows", Visibility = OpenApiVisibilityType.Important)] | |
[OpenApiSecurity("oidc_auth", | |
SecuritySchemeType.OpenIdConnect, | |
OpenIdConnectUrl = "https://login.microsoftonline.com/{tenant_id}/v2.0/.well-known/openid-configuration", | |
OpenIdConnectScopes = "openid,profile")] | |
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(IEnumerable<string>), Summary = "successful operation", Description = "successful operation")] | |
public static async Task<IActionResult> Run( | |
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = null)] HttpRequest req, | |
ILogger log) | |
{ | |
log.LogInformation("C# HTTP trigger function processed a request."); | |
var headers = req.Headers.ToDictionary(p => p.Key, p => (string) p.Value); | |
var handler = new JwtSecurityTokenHandler(); | |
var token = handler.ReadJwtToken(headers["Authorization"].Split(' ').Last()); | |
var claims = token.Claims.Select(p => p.ToString()); | |
var content = new { headers = headers, claims = claims }; | |
var result = new OkObjectResult(content); | |
return await Task.FromResult(result).ConfigureAwait(false); | |
} | |
} |
Run the function app and find the Swagger UI page.
Unlike other auth flows, this OpenID Connect auth flow shows two methods. The first one is the authentication code flow, and the other one is the implicit flow. Let's use the second one and enter the client ID value. It will redirect you to Azure Active Directory to sign in and give you the access token.
Once execute the endpoint, the access token is passed through the Authorization
header in the JWT format.
Decode and validate the token for further processing.
So far, we've covered six different ways to authenticate the HTTP trigger endpoints with the OpenAPI extension. These six ways are the most commonly used ones. Therefore, if you need, you can pick up one approach and implement it.