Skip to content

Commit

Permalink
Feature - add operation filters for Shared Access Key and Certificate…
Browse files Browse the repository at this point in the history
… authentications in OpenAPI docs (#172)

* Feature - add operation filters for Shared Access Key and Certificate authentications  in OpenAPI docs

* pr-style: remove blank line in operation filters

* Update src/Arcus.WebApi.OpenApi.Extensions/CertificateAuthenticationOperationFilter.cs

Co-authored-by: Tom Kerkhove <kerkhove.tom@gmail.com>

* Update docs/preview/features/openapi/security-definitions.md

Co-authored-by: Tom Kerkhove <kerkhove.tom@gmail.com>

* pr-sug: remove scopes in operation filters

* pr-sug: update with tests

* pr-fix: remove tests with ctor creation

* pr-sug: update w/ correct doc title

* pr-sug: make security schema name configuratble

* pr-sug: make security scheme type configurable

* pr-test: make sure we test for valid out of bound enumeration values

* pr-style: add paranthesis for AND and OR operation combinations

Co-authored-by: Tom Kerkhove <kerkhove.tom@gmail.com>
  • Loading branch information
stijnmoreels and tomkerkhove authored Aug 27, 2020
1 parent b449326 commit 207f7ac
Show file tree
Hide file tree
Showing 13 changed files with 598 additions and 144 deletions.
75 changes: 64 additions & 11 deletions docs/preview/features/openapi/security-definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ layout: default

# Adding OAuth security definition to API operations

When an API is secured via OAuth, it is helpful if the Open API documentation makes this clear via a security scheme and the API operations that require authorization automatically inform the consumer that it is possible that a 401 Unauthorized or 403 Forbidden response is returned.
The `OAuthAuthorizeOperationFilter` that is part of this package exposes this functionality.
When an API is secured via OAuth, [Shared Access Key authentication](../../features/security/auth/shared-access-key), [Certificate authentication](../../features/security/auth/certificate), it is helpful if the Open API documentation makes this clear via a security scheme and the API operations that require authorization automatically inform the consumer that it is possible that a 401 Unauthorized or 403 Forbidden response is returned.

These `IOperationFilter`'s that are part of this package exposes this functionality:
- [`CertificateAuthenticationOperationFilter`](#certificate)
- [`OAuthAuthorizeOperationFilter`](#oauth)
- [`SharedAccessKeyAuthenticationOperationFilter`](#sharedaccesskey)

## Installation

Expand All @@ -18,22 +22,71 @@ PM > Install-Package Arcus.WebApi.OpenApi.Extensions

## Usage

To indicate that an API is protected by OAuth, you need to add `AuthorizeCheckOperationFilter` as an `OperationFilter` when configuring Swashbuckles Swagger generation:
### Certificate

To indicate that an API is protected by [Certificate authentication](../../features/security/auth/certificate), you need to add `CertificateAuthenticationOperationFilter` as an `IOperationFilter` when configuring Swashbuckles Swagger generation:

```csharp
services.AddSwaggerGen(setupAction =>
{
setupAction.SwaggerDoc("v1", new Info { Title = "My API v1", Version = "v1" });

string securitySchemaName = "my-certificate";
setupAction.AddSecurityDefinition(securitySchemaName, new OpenApiSecurityScheme
{
Type = SecuritySchemeType.ApiKey
});

setupAction.OperationFilter<CertificateAuthenticationOperationFilter>(securitySchemaName);
});
```

> Note: the `CertificateAuthenticationOperationFilter` has by default `"certificate"` as `securitySchemaName`.
### OAuth

To indicate that an API is protected by OAuth, you need to add `OAuthAuthorizeOperationFilter` as an `IOperationFilter` when configuring Swashbuckles Swagger generation:

```csharp
services.AddSwaggerGen(setupAction =>
{
setupAction.SwaggerDoc("v1", new Info { Title = "My API v1", Version = "v1" });

string securitySchemaName = "my-oauth2";
setupAction.AddSecurityDefinition(securitySchemaName, new OAuth2Scheme
{
Flow = "implicit",
AuthorizationUrl = $"{authorityUrl}connect/authorize",
Scopes = scopes
});

setupAction.OperationFilter<OAuthAuthorizeOperationFilter>(securitySchemaName, new object[] { new[] { "myApiScope1", "myApiScope2" } });
});
```

> Note: the `OAuthAuthorizeOperationFilter` has by default `"oauth2"` as `securitySchemaName`.
### Shared Access Key

To indicate that an API is protected by [Shared Access Key authentication](../../features/security/auth/shared-access-key), you need to add `SharedAccessKeyAuthenticationOperationFilter` as an `IOperationFilter` when configuring Swashbuckles Swagger generation:

```csharp
services.AddSwaggerGen(setupAction =>
{
setupAction.SwaggerDoc("v1", new Info { Title = "My API v1", Version = "v1" });
setupAction.SwaggerDoc("v1", new Info { Title = "My API v1", Version = "v1" });

setupAction.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Flow = "implicit",
AuthorizationUrl = $"{authorityUrl}connect/authorize",
Scopes = scopes
});
string securitySchemaName = "my-sharedaccesskey";
setupAction.AddSecurityDefinition(securitySchemaName, new OpenApiSecurityScheme
{
Name = "X-API-Key",
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header
});

setupAction.OperationFilter<OAuthAuthorizeOperationFilter>(new object[] {new [] {"myApiScope1", "myApiScope2"});
setupAction.OperationFilter<SharedAccessKeyAuthenticationOperationFilter>(securitySchemaName);
});
```

> Note: the `SharedAccessKeyAuthenticationOperationFilter` has by default `"sharedaccesskey"` as `securitySchemaName`.
[&larr; back](/)
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.4.1" Condition="'$(TargetFramework)' == 'netcoreapp3.1'" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Arcus.WebApi.Security\Arcus.WebApi.Security.csproj" />
</ItemGroup>

<Target Name="GenerateOpenApiDocuments" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Arcus.WebApi.Security.Authentication.Certificates;
using GuardNet;
#if NETCOREAPP3_1
using Microsoft.OpenApi.Models;
#endif
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Arcus.WebApi.OpenApi.Extensions
{
/// <summary>
/// A Swashbuckle operation filter that adds certificate security definitions to authorized API operations.
/// </summary>
public class CertificateAuthenticationOperationFilter : IOperationFilter
{
private const string DefaultSecuritySchemeName = "certificate";

private readonly string _securitySchemeName;
#if NETCOREAPP3_1
private readonly SecuritySchemeType _securitySchemeType;
#endif

/// <summary>
/// Initializes a new instance of the <see cref="CertificateAuthenticationOperationFilter"/> class.
/// </summary>
/// <param name="securitySchemeName">The name of the security scheme. Default value is <c>"certificate"</c>.</param>
#if NETCOREAPP3_1
/// <param name="securitySchemeType">The type of the security scheme. Default value is <c>ApiKey</c>.</param>
#endif
public CertificateAuthenticationOperationFilter(
#if NETCOREAPP3_1
string securitySchemeName = DefaultSecuritySchemeName,
SecuritySchemeType securitySchemeType = SecuritySchemeType.ApiKey
#else
string securitySchemeName = DefaultSecuritySchemeName
#endif
)
{
Guard.NotNullOrWhitespace(securitySchemeName,
nameof(securitySchemeName),
"Requires a name for the Certificate security scheme");

_securitySchemeName = securitySchemeName;

#if NETCOREAPP3_1
Guard.For<ArgumentException>(
() => !Enum.IsDefined(typeof(SecuritySchemeType), securitySchemeType),
"Requires a security scheme type for the Certificate authentication that is within the bounds of the enumeration");

_securitySchemeType = securitySchemeType;
#endif
}

/// <summary>
/// Applies the OperationFilter to the API <paramref name="operation"/>.
/// </summary>
/// <param name="operation">The operation instance on which the OperationFilter must be applied.</param>
/// <param name="context">Provides meta-information on the <paramref name="operation"/> instance.</param>
#if NETCOREAPP3_1
public void Apply(OpenApiOperation operation, OperationFilterContext context)
#else
public void Apply(Operation operation, OperationFilterContext context)
#endif
{
bool hasOperationAuthentication =
context.MethodInfo
.GetCustomAttributes(true)
.OfType<CertificateAuthenticationAttribute>()
.Any();

bool hasControllerAuthentication =
context.MethodInfo.DeclaringType != null
&& context.MethodInfo.DeclaringType
.GetCustomAttributes(true)
.OfType<CertificateAuthenticationAttribute>()
.Any();

if (hasOperationAuthentication || hasControllerAuthentication)
{
if (operation.Responses.ContainsKey("401") == false)
{
#if NETCOREAPP3_1
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
#else
operation.Responses.Add("401", new Response { Description = "Unauthorized" });
#endif
}

if (operation.Responses.ContainsKey("403") == false)
{
#if NETCOREAPP3_1
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
#else
operation.Responses.Add("403", new Response { Description = "Forbidden" });
#endif
}

#if NETCOREAPP3_1
var scheme = new OpenApiSecurityScheme
{
Scheme = _securitySchemeName,
Type = _securitySchemeType,
In = ParameterLocation.Header
};

operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[scheme] = new List<string>()
}
};
#else
operation.Security = new List<IDictionary<string, IEnumerable<string>>>
{
new Dictionary<string, IEnumerable<string>> { [_securitySchemeName] = Enumerable.Empty<string>() }
};
#endif
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,26 @@ namespace Arcus.WebApi.OpenApi.Extensions
/// </summary>
public class OAuthAuthorizeOperationFilter : IOperationFilter
{
private readonly string _securitySchemaName;
private readonly IEnumerable<string> _scopes;

/// <summary>
/// Initializes a new instance of the <see cref="OAuthAuthorizeOperationFilter"/> class.
/// </summary>
/// <param name="scopes">A list of API scopes that is defined for the API that must be documented.</param>
/// <param name="securitySchemaName">The name of the security schema. Default value is <c>"oauth2"</c>.</param>
/// <remarks>It is not possible right now to document the scopes on a fine grained operation-level.</remarks>
/// <exception cref="ArgumentNullException">When the <paramref name="scopes"/> are <c>null</c>.</exception>
/// <exception cref="ArgumentException">When the <paramref name="scopes"/> has any elements that are <c>null</c> or blank.</exception>
public OAuthAuthorizeOperationFilter(IEnumerable<string> scopes)
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="scopes"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">
/// Thrown when the <paramref name="scopes"/> has any elements that are <c>null</c> or blank or the <paramref name="securitySchemaName"/> is blank.
/// </exception>
public OAuthAuthorizeOperationFilter(IEnumerable<string> scopes, string securitySchemaName = "oauth2")
{
Guard.NotNull(scopes, nameof(scopes), "The sequence of scopes cannot be null");
Guard.For<ArgumentException>(() => scopes.Any(String.IsNullOrWhiteSpace), "The sequence of scopes cannot contain a scope that is null or blank");

Guard.NotNull(scopes, nameof(scopes), "Requires a list of API scopes");
Guard.For<ArgumentException>(() => scopes.Any(String.IsNullOrWhiteSpace), "Requires a list of non-blank API scopes");
Guard.NotNullOrWhitespace(securitySchemaName, nameof(securitySchemaName), "Requires a name for the OAuth2 security scheme");

_securitySchemaName = securitySchemaName;
_scopes = scopes;
}

Expand All @@ -44,12 +50,24 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
public void Apply(Operation operation, OperationFilterContext context)
#endif
{
var hasAuthorize = context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
(
context.MethodInfo.DeclaringType != null &&
context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() &&
context.MethodInfo.GetCustomAttributes(false).OfType<AllowAnonymousAttribute>().Any() == false
);
bool operationHasAuthorizeAttribute =
context.MethodInfo.GetCustomAttributes(inherit: true)
.OfType<AuthorizeAttribute>()
.Any();

bool controllerHasAuthorizeAttribute =
context.MethodInfo.DeclaringType != null
&& context.MethodInfo.DeclaringType.GetCustomAttributes(inherit: true)
.OfType<AuthorizeAttribute>()
.Any();

bool operationHasAllowAnonymousAttribute =
context.MethodInfo.GetCustomAttributes(inherit: false)
.OfType<AllowAnonymousAttribute>().Any();

bool hasAuthorize =
operationHasAuthorizeAttribute
|| (controllerHasAuthorizeAttribute && !operationHasAllowAnonymousAttribute);

if (hasAuthorize)
{
Expand All @@ -74,7 +92,7 @@ public void Apply(Operation operation, OperationFilterContext context)
#if NETCOREAPP3_1
var oauth2Scheme = new OpenApiSecurityScheme
{
Scheme = "oauth2",
Scheme = _securitySchemaName,
Type = SecuritySchemeType.OAuth2
};

Expand All @@ -88,7 +106,7 @@ public void Apply(Operation operation, OperationFilterContext context)
#else
operation.Security = new List<IDictionary<string, IEnumerable<string>>>
{
new Dictionary<string, IEnumerable<string>> { ["oauth2"] = _scopes }
new Dictionary<string, IEnumerable<string>> { [_securitySchemaName] = _scopes }
};
#endif
}
Expand Down
Loading

0 comments on commit 207f7ac

Please sign in to comment.