Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
tomkerkhove marked this conversation as resolved.
Show resolved Hide resolved
- [`CertificateAuthenticationOperationFilter`](#certificate)
- [`OAuthAuthorizeOperationFilter`](#oauth)
- [`SharedAccessKeyAuthenticationOperationFilter`](#sharedaccesskey)

## Installation

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

## Usage
tomkerkhove marked this conversation as resolved.
Show resolved Hide resolved

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
tomkerkhove marked this conversation as resolved.
Show resolved Hide resolved

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