Patrick Szalapski
Patrick Szalapski

Reputation: 9439

How do I set up Swashbuckle and Swagger UI to Authorize using Open ID Connect Discovery so it can supply the right Bearer token?

How do I set up Swashbuckle and Swagger UI to Authorize using Open ID Connect Discovery (in my case, to Azure AD)?

Here's my (not-fully-working) SwaggerGen setup so far, based on https://stackoverflow.com/a/66147996/7453 :

SwaggerGenOptions c => {
    OpenApiInfo apiInfo = new()  { /* ...snip... */ };
    c.SwaggerDoc("v1", apiInfo);
    IncludeXmlFileForSwagger(c);

    // Defines the Open ID Connect discovery scheme - see also https://stackoverflow.com/a/66147996/7453
    OpenApiSecurityScheme mainScheme = new()
    {
        Type = SecuritySchemeType.OpenIdConnect,
        OpenIdConnectUrl = new Uri($"https://login.microsoftonline.com/{myTenantId}/.well-known/openid-configuration"),
    };
    c.AddSecurityDefinition("OpenIdConnect", mainScheme);

    // Adds a reference to the above scheme as required for every API action (we can get more nuanced later)
    //   Note: if just try to use mainScheme instead, it doesn't apply a Bearer token)
    OpenApiSecurityScheme securityScheme = new() {
        Reference = new OpenApiReference {
            Type = ReferenceType.SecurityScheme, Id = "OpenIdConnect"
        }
    };
    OpenApiSecurityRequirement securityRequirements = new() { {securityScheme, Array.Empty<string>()} };
    c.AddSecurityRequirement(securityRequirements);
}

...and in the static configure method, I have...

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyService v1");
    c.OAuthClientId(myClientId);
    c.OAuthUsePkce();
});

So the above code lets me authenticate and check a scope called "openid" and shows the "locked" padlock". When I try to run a action via the Swagger UI, it supplies a Bearer token but it comes back invalid.

Do I need to somehow specify an explicit named scope, rather than "openid"? How?

Is my setup even right? If so, why isn't the token valid? Any ideas for troubleshooting?

Upvotes: 7

Views: 4273

Answers (2)

Swashbuckle acts as a client of OIDC server to get access token. Hence, first you should add Redirect Uri to allow list <origin>/swagger/oauth2-redirect.html (example, https://localhost:<port>/swagger/oauth2-redirect.htmlfor local development. Swashbuckle will create and CORS (not direct) request to get content of discovery documents. So that you should ensure if CORS policy allows Swashbuckle origin.

Upvotes: 1

Patrick Szalapski
Patrick Szalapski

Reputation: 9439

Since Swagger UI is using the web browser context to make the requests, I found it easier and way simpler to just provide a link at the top that will bring them to any API call that requires auth, and then also add a security requirement on all functions that require authorization.

This will work if your API automatically redirects and uses browser functionality to sign in, as I think most do. After the user signs in, all future HTTP requests from the Swagger UI will send the auth cookie just as it would for hitting the endpoints in the browser directly.

First, the Swagger config in Startup.cs, including a link to make signing in user-friendly:

services.AddSwaggerGen(c => {
    OpenApiInfo apiInfo = new()
    {
        Title = "MyService",
        Version = "v1",
        Description = "<p>An API for working with ... "
            + "<p>If you get '<b>Failed to fetch</b>' below on an action that shows a padlock icon, this likely "
            + "means you are not <b>signed in</b>, so "
            + "<a target=\"_blank\" href=\"/api/v1/security/signIn\">sign in here</a>, then "
            + "your sign-in will take effect for any action below.",
    };
    c.SwaggerDoc("v1", apiInfo);

    /* put other configuration here, such as c.IncludeXmlComments */

    c.OperationFilter<MethodNeedsAuthorizationFilter>(); // puts auth UI on the right actions                                                             
});

In the example above, my endpoint /api/v1/security/signIn requires authorization; you can use any endpoint of yours that requires authorization.

Then here's the MethodNeedsAuthorizationFilter you need to enable showing the open padlock icon appropriately:

using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections.Generic;
using System.Linq;
namespace GeneralMills.TradePlannerService;
/// <summary>
/// Provides a method that applies Swagger UI security requirements to all controller actions that need authorization.
/// </summary>
public class MethodNeedsAuthorizationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        if (operation is null) throw new ArgumentNullException(nameof(operation));
        if (context is null) throw new ArgumentNullException(nameof(context));

        object[] methodAttributes = context.MethodInfo.GetCustomAttributes(true);

        bool needsAuth =
            methodAttributes.OfType<AuthorizeAttribute>().Any()
            || (context.MethodInfo.DeclaringType != null
                && context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any()
                && !methodAttributes.OfType<AllowAnonymousAttribute>().Any());

        if (needsAuth)
        {
            OpenApiSecurityScheme scheme = new()
            {
                Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "ExampleName" },
                Scheme = "ExampleName",
                Name = "ExampleName",
                In = ParameterLocation.Header,
            };

            operation.Security = new List<OpenApiSecurityRequirement>()
            {
                new () { {scheme, new List<string>() } }
            };
        }
    }
}

This effectively circumvents Swagger UI's built-in authorization support, which I could not get working.

Upvotes: 3

Related Questions