This post show how to customize Swagger UI in a Blazor WASM project using Swashbuckle library, implement a custom authentication UI and manage api visibility based on user roles.

Swagger UI is a very powerful api documentation library but it does not implement out of the box visibility rules for api methods, it show by default all methods to all users.

With this article we manage 2 main problems:

  • Show api documentation only for authenticated users with a custom UI who acquire user credentials and generate a JWT token.
  • Filter api methods visibility based on user role.

Swagger UI page with login:

API visibility after login:

Full project on GitHub: https://github.com/CodeDesignTips/CustomBlazorAuthentication

Table of contents:

Add Swashbuckle libraries to the project

Add these NuGet Packages in the server project:

Configure Swagger service in Startup.cs

Register Swagger in ConfigureServices method adding these custom filters:

SwaggerAuthorizeRoleFilter: Show methods based on user roles

SwaggerAppendAuthorizeToSummaryOperationFilter: Add note on roles for methods requiring authentication

SwaggerIgnoreFilter: Ignore DTO properties with [SwaggerIgnore] attribute

ResponseHeaderFilter: Show response header documentation

SwaggerAuthenticatedDescriptionFilter: Set swagger description based on authenticated or anonymous status

// Register the Swagger generator, defining 1 or more Swagger documents
var commonDescription = "**Type here your swagger login page title**";
var swaggerAuthenticatedDescription = commonDescription;
swaggerAuthenticatedDescription += "\r\n";
swaggerAuthenticatedDescription += "\r\nType here your swagger page description";

var swaggerDescription = commonDescription;
swaggerDescription += "\r\n";
swaggerDescription += "\r\nTo access integration api login with user: demo password: demo";

services.AddSwaggerGen(c =>
{
	c.SwaggerDoc("v1", new OpenApiInfo
	{
		Title = "CustomBlazorAuthentication API",
		Contact = new OpenApiContact
		{
			Name = "CodeDesignTips.com",
			Email = "info@codedesigntips.com",
			Url = new Uri("https://www.codedesigntips.com")
		},
		Description = swaggerDescription,
		Version = "v1"
	});

	//Resolve apiDescriptions conflict
	c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
	
	// Set the comments path for the Swagger JSON and UI.
	var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
	var xmlFilePath = Path.Combine(AppContext.BaseDirectory, xmlFile);
	if (File.Exists(xmlFilePath))
		c.IncludeXmlComments(xmlFilePath);

	//Settings for token authentication
	c.AddSecurityDefinition("Bearer",
		new OpenApiSecurityScheme
		{
			In = ParameterLocation.Header,
			Description = "Please enter into field the word 'Bearer' following by space and JWT",
			Name = "Authorization",
			Type = SecuritySchemeType.ApiKey,
			Scheme = "Bearer"
		});
	c.AddSecurityRequirement(new OpenApiSecurityRequirement()
	{
		{
			new OpenApiSecurityScheme
			{
				Reference = new OpenApiReference
				{
					Type = ReferenceType.SecurityScheme,
					Id = "Bearer"
				},
				Scheme = "oauth2",
				Name = "Bearer",
				In = ParameterLocation.Header
			},
			new List<string>()
		}
	});

	//Add note on roles for methods requiring authentication
	c.OperationFilter<SwaggerAppendAuthorizeToSummaryOperationFilter>();
	//Filter user visible methods
	c.DocumentFilter<SwaggerAuthorizeRoleFilter>();
	//Ignore properties with [SwaggerIgnore] attribute
	c.SchemaFilter<SwaggerIgnoreFilter>();
	//Filter to manager response header documentation
	c.OperationFilter<ResponseHeaderFilter>();

	c.DocumentFilter<SwaggerAuthenticatedDescriptionFilter>(swaggerDescription, swaggerAuthenticatedDescription);
});

Enable Swagger in Configure method and inject custom css and js:

// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();

// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "CustomBlazorAuthentication API v1");
    c.InjectStylesheet("/swagger/swagger.css");
    c.InjectJavascript("/swagger/swagger.js");
    c.DefaultModelsExpandDepth(-1);
});

Add custom css and js

In client wwwroot create the swagger folder, put swagger.css and swagger.js inside.

With swagger.css we customize the Swagger logo image

#swagger-ui img[alt="Swagger UI"]
{
    display: block;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    content: url('/images/logo.png');
    max-width: 100%;
    max-height: 100%;
}

With swagger.js we add the authentication ui with user and password and reload the page with filtered api visibility on user login.

(function ()
{
    const overrider = () =>
    {
        const swagger = window.ui;
        if (!swagger) 
        {
            console.error('Swagger wasn\'t found');
            return;
        }

        ensureAuthorization(swagger);
        reloadSchemaOnAuth(swagger);
        clearInputPlaceHolder(swagger);
        showLoginUI(swagger);
    }

    const getAuthorization = (swagger) => swagger.auth()._root.entries.find(e => e[0] === 'authorized');
    const isAuthorized = (swagger) =>
    {
        const auth = getAuthorization(swagger);
        return auth && auth[1].size !== 0;
    };

    // a hacky way to append authorization header - we are basically intercepting 
    // all requests, if no authorization was attached while user did authorized himself,
    // append token to request
    const ensureAuthorization = (swagger) => 
    {
        // retrieve bearer token from authorization
        const getBearer = () => 
        {
            const auth = getAuthorization(swagger);
            const def = auth[1]._root.entries.find(e => e[0] === 'Bearer');
            if (!def)
                return undefined;

            const token = def[1]._root.entries.find(e => e[0] === 'value');
            if (!token)
                return undefined;

            return token[1];
        }

        // override fetch function of Swagger to make sure
        // that on every request of the client is authorized append auth-header
        const fetch = swagger.fn.fetch;
        swagger.fn.fetch = (req) => 
        {
            if (!req.headers.Authorization && isAuthorized(swagger)) 
            {
                const bearer = getBearer();
                if (bearer) 
                {
                    req.headers.Authorization = bearer;
                }
            }
            return fetch(req);
        }
    };
    // makes that once user triggers performs authorization,
    // the schema will be reloaded from backend url
    const reloadSchemaOnAuth = (swagger) => 
    {
        const getCurrentUrl = () => 
        {
            const spec = swagger.getState()._root.entries.find(e => e[0] === 'spec');
            if (!spec)
                return undefined;

            const url = spec[1]._root.entries.find(e => e[0] === 'url');
            if (!url)
                return undefined;

            return url[1];
        }
        const reload = () => 
        {
            const url = getCurrentUrl();
            if (url) 
            {
                swagger.specActions.download(url);
            }
        };

        const handler = (caller, args) => 
        {
            const result = caller(args);
            if (result.then) 
            {
                result.then(() => reload())
            }
            else
            {
                reload();
            }
            return result;
        }

        const auth = swagger.authActions.authorize;
        swagger.authActions.authorize = (args) => handler(auth, args);
        const logout = swagger.authActions.logout;
        swagger.authActions.logout = (args) => handler(logout, args);
    };
    /**
     * Reset input element placeholder
     * @param {any} swagger
     */
    const clearInputPlaceHolder = (swagger) =>
    {
        //https://github.com/api-platform/core/blob/main/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js#L6-L41
        new MutationObserver(function (mutations, self)
        {
            var elements = document.querySelectorAll("input[type=text]");
            for (var i = 0; i < elements.length; i++)
                elements[i].placeholder = "";
        }).observe(document, { childList: true, subtree: true });
    }
    /**
     * Show login UI
     * @param {any} swagger
     */
    const showLoginUI = (swagger) =>
    {
        //https://github.com/api-platform/core/blob/main/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js#L6-L41
        new MutationObserver(function (mutations, self)
        {
            var rootDiv = document.querySelector("#swagger-ui > section > div.swagger-ui > div:nth-child(2)");
            if (rootDiv == null)
                return;

            var informationContainerDiv = rootDiv.querySelector("div.information-container.wrapper");
            if (informationContainerDiv == null)
                return;

            var descriptionDiv = informationContainerDiv.querySelector("section > div > div > div.description");
            if (descriptionDiv == null)
                return;

            var loginDiv = descriptionDiv.querySelector("div.login");
            if (loginDiv != null)
                return;

            //Check authentication
            if (isAuthorized(swagger))
                return;

            //Remove elements different from information-container wrapper
            for (var i = 0; i < rootDiv.children.length; i++)
            {
                var child = rootDiv.children[i];
                if (child !== informationContainerDiv)
                    child.remove();
            }

            //Create UI di login
            createLoginUI(descriptionDiv);
            
        }).observe(document, { childList: true, subtree: true });

        /**
         * Create login ui elements
         * @param {any} rootDiv
         */
        const createLoginUI = function (rootDiv)
        {
            var div = document.createElement("div");
            div.className = "login";

            rootDiv.appendChild(div);

            //UserName
            var userNameLabel = document.createElement("label");
            div.appendChild(userNameLabel);

            var userNameSpan = document.createElement("span");
            userNameSpan.innerText = "User";
            userNameLabel.appendChild(userNameSpan);
            
            var userNameInput = document.createElement("input");
            userNameInput.type = "text";
            userNameInput.style = "margin-left: 10px; margin-right: 10px;";
            userNameLabel.appendChild(userNameInput);

            //Password
            var passwordLabel = document.createElement("label");
            div.appendChild(passwordLabel);

            var passwordSpan = document.createElement("span");
            passwordSpan.innerText = "Password";
            passwordLabel.appendChild(passwordSpan);

            var passwordInput = document.createElement("input");
            passwordInput.type = "password";
            passwordInput.style = "margin-left: 10px; margin-right: 10px;";
            passwordLabel.appendChild(passwordInput);

            //Login button
            var loginButton = document.createElement("button")
            loginButton.type = "submit";
            loginButton.type = "button";
            loginButton.classList.add("btn");
            loginButton.classList.add("auth");
            loginButton.classList.add("authorize");
            loginButton.classList.add("button");
            loginButton.innerText = "Login";
            loginButton.onclick = function ()
            {
                var userName = userNameInput.value;
                var password = passwordInput.value;

                if (userName === "" || password === "")
                {
                    alert("Insert userName and password!");
                    return;
                }

                login(userName, password);
            };

            div.appendChild(loginButton);
        }
        /**
         * Manage login
         * @param {any} userName UserName
         * @param {any} password Password
         */
        const login = function (userName, password)
        {
            var xhr = new XMLHttpRequest();

            xhr.onreadystatechange = function ()
            {
                if (xhr.readyState == XMLHttpRequest.DONE)
                {
                    if (xhr.status == 200 || xhr.status == 400)
                    {
                        var response = JSON.parse(xhr.responseText);
                        if (!response.Result)
                        {
                            alert(response.ErrorMessage);
                            return;
                        }

                        var accessToken = response.AccessToken;

                        var obj = {
                            "Bearer": {
                                "name": "Bearer",
                                "schema": {
                                    "type": "apiKey",
                                    "description": "Please enter into field the word 'Bearer' following by space and JWT",
                                    "name": "Authorization",
                                    "in": "header"
                                },
                                value: "Bearer " + accessToken
                            }
                        };

                        swagger.authActions.authorize(obj);
                    }
                    else
                    {
                        alert('error ' + xhr.status);
                    }
                }
            };

            xhr.open("POST", "/Authentication/Login", true);
            xhr.setRequestHeader("Content-Type", "application/json");

            var json = JSON.stringify({ "UserName": userName, "Password": password });

            xhr.send(json);
        }
    }

    // append to event right after SwaggerUIBundle initialized
    window.addEventListener('load', () => setTimeout(overrider, 0), false);
}());

References

https://stackoverflow.com/questions/38070950/generate-swashbuckle-api-documentation-based-on-roles-api-key

https://github.com/jenyayel/SwaggerSecurityTrimming

https://github.com/api-platform/core/blob/main/src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js#L6-L41


0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *