Azure Active Directory with Swashbuckle in ASP.NET Core

Swashbuckle is a great tool to create documentation of your APIs developed with ASP.NET Core. I had a blog post before about Swagger in ASP.Net core in general here but today I am going to talk aboutĀ Azure Active Directory with Swashbuckle in ASP.NET Core.

Swashbuckle, under the hood uses Swagger and Swagger UI but abstracts us from installing and configuring those two products. We just have to install the NuGet Swashbuckle.AspNetCore package and we have everything we need.

The package itself incorporates a version of Swagger UI and the only thing we have to do is to introduce a couple of lines in our Startup class. The first one in the ConfigureServices :

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new Info { Title = "Your API", Version = "v2" });
});

With this line, we enable the generation of documentation. The second step activates the Swagger UI endpoint to view this documentation. To do this we must add the following lines in the Configure method of the Startup class:

app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v2/swagger.json", "Your Api");
});

And with this, we already have the automatic documentation for our API. Simply navigate to / Swagger / UI to see it (and try our API!).

Adding support for Azure Active Directory

By default Swagger, UI does not use any type of authentication, so if our API is authenticated, the test calls that Swagger UI performs will receive a 401. Swagger UI (and Swashbuckle) supports integration on OAuth2, although in the version used by Swashbuckle there is no direct support for OIDC, since OIDC is a layer above OAuth2, more or less we can mount it. Let’s go by parts.

The first modification must be made in the B2C itself. A characteristic of B2C is that the callback URLs of the clients must be complete, that is, it is not only the origin. That means that if you indicate that the callback URL of an application is http://localhost: 5000/, B2C will only accept that callback URL. It will not accept another such as http://localhost:5000/ callback-result, p. ex.

When we configure Swagger UI (through the methods offered by Swashbuckle, because we do not have access to the internal Swagger UI) to use OAuth2, it is the Swagger UI itself that manages the callback URL and that URL is /swagger-ui/o2c.html . So we must add that callback URL to the URLs accepted by B2C.

With this, we have the application configured for B2C to accept the callback URL that it will use. Now the next point is to configure Swagger UI. To do this we must use the ConfigureSwaggerGen method in the ConfigureServices method and add a Security Descriptor, which is what Swagger UI tells us that our API is secured. With this, Swagger UI will display an “Authorize” button that will launch the defined security flow:

services.ConfigureSwaggerGen(swaggerGen =>
{
    var authority = string.Format("https://login.microsoftonline.com/{0}/oauth2/v2.0", tenant);
    swaggerGen.AddSecurityDefinition("Swagger", new OAuth2Scheme
    {
        AuthorizationUrl = authority + "/authorize",
        Flow = "implicit",
        TokenUrl = authority + "/connect/token",
        Scopes = new Dictionary<string, string>
        {
            { "openid","User offline" },
        }
    });
});

Let’s go by parts. The tenant variable contains the B2C tenant (usually something like eiximenis.onmicrosoft.com) Finally in Scopes we define the list of scopes orAuth that we are going to ask for, in this case, we only need openid If our API had additional scopes defined in B2C We should ask for them Swagger UI will show us this list of scopes when we press the Authorize button and we can choose the ones we want to obtain.

The rest of the parameters are the authorization endpoints and the flow to use.

Since we want to use OAuth 2, we must configure Swagger UI so that it knows the parameters to use and thus generate the final URLs with all the parameters. Well, in fact you only need one that is the client_id (the client identifier of the API itself in the B2C). To do this we are going to add a call to ConfigureOAuth2 within the code in the UseSwaggerUI method :

 app.UseSwaggerUI(c =>
    {
        var b2cConfig = Configuration.GetSection("b2c");
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "Bookings Api");
        c.ConfigureOAuth2(clientId,"a", "b", "b","",
            new
            {
                p = "your policy",
                prompt = "login",
                nonce = "defaultNonce"
            });
    });
    app.UseMvc();
}

Let’s go in parts: The variable clientId contains the client ID. It is the only thing we need. The other parameters of ConfigureOAuth2 (except the last one we are going to talk about now) could be null (or empty string). But if in the first two you put null or empty string, you will receive a JavaScript error in the JS generated by Swagger UI. That’s why I use “a”, “b” and “b” as parameters.

The last parameter is an anonymous object that contains the query string of the URL to be generated. In this case, we add parameters of B2C. The most important is the parameter p that defines the policy to be used in B2C. In my case I use ” B2C_1_SignUpInPolicy ” because that’s what my policy is called, there you must use your “Sign-in” policy that you have defined in B2C and want to use. The value of the two other parameters ( nonce and prompt ) have been taken from the login URL used by B2C (you can see it if, in the B2C configuration portal, you go to the details of the policy).

That should work for you, but it will not work. The reason is that Swagger UI always uses the query string parameter “response_type = token”, whereas B2C requires “response_type = id_token”. The reason that Swagger UI uses “response_type = token” is because that is the value that OAuth2 specifies for the implicit flow. But B2C uses OIDC, and in OIDC the implicit flow uses “response_type = id_token”.

The version of Swagger UI that is incorporated in Swashbuckle does not support OIDC, so we have no way to change the “response_type = token” to “response_type = id_token” in the URL that Swagger UI generates. Luckily, where we did not arrive from the server with C # we arrived from the client with JavaScript.

Remember that Swagger UI at the end runs in your browser. When you press the button to authenticate in Swagger UI, it generates the URL and then uses window.open to open a new window with that URL. That new window is the one that manages the authentication flow. So a solution (roughly but that works) is to replace the window.open function with another that does the same but replace the token with id_token in the URL :

window.swaggerUiAuth = window.swaggerUiAuth || {};
window.swaggerUiAuth.tokenName = 'id_token';
if (!window.isOpenReplaced) {
    window.open = function (open) {
        return function (url) {
            url = url.replace('response_type=token', 'response_type=id_token');
            return open.call(window, url);
        };
    }(window.open);
    window.isOpenReplaced = true;
}

This code does the trick. Of course, now we have to know how we can “inject” Swagger UI. If we had installed Swagger UI it would be easy, but it is not our case: with Swashbuckle, Swagger UI is embedded inside, we do not see the Swagger UI JavaScript files. Luckily Swagger UI offers a mechanism to “inject” external JS and Swashbuckle support that mechanism. Just add the following line within the UseSwaggerUI method:

c.InjectOnCompleteJavaScript("/swagger-ui-b2c.js");

The swagger-ui-b2c.js file contains the above code. Of course, make sure that this file is in wwwroot in the route you want (I have put it in the root, but that as you prefer).

And now, yes! Now you can authenticate your API against AAD B2C using Swagger UI.

Note: Swagger currently supports OIDC, including OIDC discovery (of which one day we will talk), it is the “embedded” version in Swashbuckle that does not support it.

You may also like...

1 Response

  1. Nathan says:

    Great post been looking for it for a while, just want to share my experience remember to add the “/” in InjectOnCompleteJavaScript method or else is not going to work

Leave a Reply

Your email address will not be published.