Skip to content

Latest commit

 

History

History
243 lines (162 loc) · 10.8 KB

adapter-host-authn-authz.md

File metadata and controls

243 lines (162 loc) · 10.8 KB

Adapter Host Authentication and Authorization

Note: this document assumes that you have used the project template for Visual Studio and dotnet new to create an adapter host. See here for more information.

By default, adapter hosts created using the project template for Visual Studio and dotnet new do not enforce any kind of authentication or authorization on adapter APIs.

In scenarios where the adapter host runs on the same machine as App Store Connect, it may be sufficient to restrict the adapter host to listen on localhost addresses only and let App Store Connect handle authorization to perform actions such as reading and writing tag values. If the adapter host is accessible on non-localhost addresses, it may also be sufficient to use firewall rules or request filtering rules in the host itself to only allow requests from safe-listed IP addresses.

Authentication

In scenarios where you require authentication on your adapter host, App Store Connect supports the following authentication types:

  • X.509 client certificates
  • Windows Authentication (not available for gRPC due to incompatibility with HTTP/2)
  • Azure AD bearer tokens using client credentials
  • JWT bearer tokens issued by App Store Connect

X.509 Client Certificate Authentication

When using X.509 certificate authentication, the certificate represents App Store Connect itself rather than the calling user.

To enable X.509 client certificate authentication (including receiving certificates via HTTP request headers instead of at the TLS level), follow Microsoft's documentation here.

Windows Authentication Authentication

When using Windows Authentication, App Store Connect will always authenticate using the indentity of the App Store Connect service rather than the identity of the calling user.

To enable Windows Authentication, follow Microsoft's documentation here.

Azure AD Bearer Token Authentication

App Store Connect requests bearer tokens from Azure AD using client credentials that represent the App Store Connect itself rather than the calling user.

To use Azure AD bearer token authentication, you must perform the following pre-requisite steps:

  1. Create an Azure AD app registration that represents the adapter host. The adapter host app registration must be configured to expose an API, as the application ID URI generated by Azure AD will be used by App Store Connect when requesting bearer tokens.

  2. Create an Azure AD app registration that represents App Store Connect. The App Store Connect app registration must be granted application-level access to the adapter host app registration by a cloud administrator.

  3. Create a client secret for the App Store Connect app registration, or create and upload an X.509 client certificate that will be used for authentication.

To enable Azure AD bearer token authentication in the adapter host:

  1. Add version 2.0.7-preview or later of the Microsoft.Identity.Web NuGet package to the adapter host project.

  2. Add the following JSON to the adapter host's appsettings.json configuration file:

    {
      "Authentication": {
        "AzureAd": {
          "Instance": "https://login.microsoftonline.com/",
          "TenantId": "<YOUR_TENANT_ID>",
          "ClientId": "<ADAPTER_HOST_AAD_APP_ID>"
        }
      }
    }
  3. Add Azure AD authentication at application startup:

    builder.Services
        .AddAuthentication()
        .AddMicrosoftIdentityWebApi(builder.Configuration, "Authentication:AzureAd");
  4. Configure Azure AD authentication in App Store Connect, specifying the application ID URI for the adapter host as the resource URI.

App Store Connect Bearer Token Authentication

Bearer tokens issued by App Store Connect can represent either the calling Industrial App Store user or the App Store Connect itself, depending on whether the request was initiated by a user or by the system.

App Store Connect can be configured to issue per-call JWT bearer tokens that represent the identity of the calling Industrial App Store user. A shared secret key is used by App Store Connect to sign the tokens, and by the adapter host when validating tokens.

To enable App Store Connect JWT bearer tokens in the adapter host:

  1. Add version 7.0.0 or later of the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package to the adapter host project.

  2. Generate a shared secret key and print it to the console using the following PowerShell commands:

    $key = New-Object Byte[] 32
    [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($key)
    [Convert]::ToBase64String($key)
  3. Make a note of the base64-encoded key printed by the above command.

  4. Run the following commands from PowerShell in the directory for the adapter host project:

    dotnet user-secrets set "Authentication:Schemes:Bearer:SigningKeys:0:Value" "<base64_secret_key>"
    dotnet user-secrets set "Authentication:Schemes:Bearer:SigningKeys:0:Issuer" "App Store Connect"
    
  5. Add the following JSON to the adapter host's appsettings.json configuration file:

    {
      "Authentication": {
        "Schemes": {
          "Bearer": {
            "ValidAudience": "https://localhost:<YOUR_SSL_PORT>",
            "ValidIssuer": "App Store Connect"
          }
        }
      }
    }
  6. Add bearer token authentication at application startup:

    builder.Services
        .AddAuthentication()
        .AddJwtBearer();
  7. Configure JWT authentication in App Store Connect using the shared secret key generated above.

You may also customise the required audience in the adapter host configuration settings if preferred. Remember to configure App Store Connect to generate tokens for this audience if you choose to do this!

Note that these instructions use dotnet user-secrets to store the shared secret in a development environment. This ensures that the secret key is not added to source control by mistake. In a production environment, secret keys should be stored using a service such as Azure Key Vault, or in another secure location.

Authorization

Authorization can be applied in three ways:

  • At the route level, using standard ASP.NET Core authorization.
  • At the application level, using a custom FeatureAuthorizationHandler to authorize access to individual features on adapters.
  • At the adapter level, by examining the IAdapterCallContext passed to adapter operations.

Route Authorization

To apply authorization at the API-level, you must add authorization requirements to adapter API endpoints when starting your web application. For example, to require an authenticated user for all adapter API endpoints:

// MVC controllers
app.MapControllers().RequireAuthorization();

// Minimal API routes (ASP.NET Core >= 7.0 only)
app.MapDataCoreAdapterApiRoutes().RequireAuthorization();

// SignalR hubs
app.MapDataCoreAdapterHubs((type, builder) => builder.RequireAuthorization());

// gRPC services
app.MapDataCoreGrpcServices((type, builder) => builder.RequireAuthorization());

Note that for SignalR and gRPC-based APIs it is possible to configure authorization separately on each individual hub or service. The callback passed to MapDataCoreAdapterHubs and MapDataCoreGrpcServices receives the type of the hub or service that is being configured, and an IEndpointConventionBuilder that is used to configure authorization requirements for that item.

Adapter Feature Authorization

In addition to route-level authorization performed by ASP.NET Core, all adapter API route handlers also perform application-level authorization to ensure that the caller is permitted to access individual features on an adapter. The default behaviour is to allow all callers access to all features, but you can customise this behaviour by extending the FeatureAuthorizationHandler class:

class MyFeatureAuthHandler : FeatureAuthorizationHandler {

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, FeatureAuthorizationRequirement requirement, IAdapter resource) {
        // Require at least one authenticated identity.
        if (!context.User.Identities.Any(x => x.IsAuthenticated)) {
            context.Fail();
            return;
        }

        if (!await AuthorizeAsync(resource, requirement.FeatureUri, context.User).ConfigureAwait(false)) {
            context.Fail();
            return;
        }

        context.Succeed(requirement);
    }

    private async Task<bool> AuthorizeAsync(IAdapter adapter, Uri? featureUri, ClaimsPrincipal user) {
        // TODO: add custom logic to test if the calling user is allowed to access the specified feature on the adapter. 
        // 
        // The featureUri argument will be null when the requirement is simply that the adapter is
        // visible to the caller (for example, when the caller is requesting metadata about the 
        // adapter).
    }
}

The handler is then registered with the IAdapterConfigurationBuilder at startup:

builder.Services
    .AddDataCoreAdapterAspNetCoreServices()
    ...
    .AddAdapterFeatureAuthorization<MyFeatureAuthHandler>();

Adapter Operation Authorization

All adapter feature methods accept an IAdapterCallContext parameter that encapsulates information about the operation, including the caller (via the User property).

You can use this information to apply bespoke authorization in your adapter implementation. You can also use this information to implement logic restricting access to different tags within the adapter based on the identity of the caller:

partial class MyAdapter : IReadSnapshotTagValues {

    public async IAsyncEnumerable<TagValueQueryResult> ReadSnapshotTagValues(
        IAdapterCallContext context, 
        ReadSnapshotTagValuesRequest request, 
        [EnumeratorCancellation]
        CancellationToken cancellationToken
    ) {
        foreach (var tag in request.Tags) {
            if (!await CanReadTagAsync(tag, context.User, cancellationToken)) {
                continue;
            }

            yield return GetCurrentValue(tagNameOrId);
        }
    }


    private async Task<bool> CanReadTagAsync( 
        string tagNameOrId, 
        ClaimsPrincipal? user,
        CancellationToken cancellationToken
    ) {
        // TODO: implementation
    }


    private TagValueQueryResult GetCurrentValue(string tagNameOrId) {
        // TODO: implementation
    }

}