Configuring Bearer Authentication with Keycloak for Swagger UI

In this blog post, we'll walk through configuring Swagger UI to use Bearer Authentication with Keycloak, focusing on both Authorization Code and Client Credentials flows. This guide will help you secure your API documentation and ensure that only authenticated users can access your APIs.


Introduction

Swagger UI is a powerful tool for documenting and interacting with RESTful APIs. When working with secured APIs, it's essential to configure Swagger UI to handle authentication. In this tutorial, we'll set up Bearer Authentication with Keycloak, a popular open-source Identity and Access Management solution, for Swagger UI using the springdoc-openapi-starter-webflux-ui package in a Spring Boot WebFlux application.


1. Setting Up Keycloak

Before configuring Swagger UI, ensure you have Keycloak set up:

  1. Create a Keycloak Realm:

    • Log in to the Keycloak admin console.

    • Create a new realm or use an existing one.

  2. Create a Keycloak Client:

    • Go to the "Clients" section.

    • Click "Create" and configure your client:

      • Client ID: Your application's client ID.

      • Client Protocol: openid-connect.

      • Access Type: For Authorization Code flow, set it to confidential. For Client Credentials flow, also use confidential.

  3. Configure Client Credentials:

    • Under the "Credentials" tab, note down the Client Secret.
  4. Set Redirect URIs (if required):

    • For Authorization Code flow, add the redirect URIs as needed.

2. Configure Swagger UI with Bearer Authentication

Authorization Code Flow

For user authentication via OAuth2 Authorization Code flow:

Update OpenApiConfig Class:

package com.example.config;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.models.security.OAuthFlow;
import io.swagger.v3.oas.models.security.OAuthFlows;

@OpenAPIDefinition(
        info = @Info(
                contact = @Contact(
                        name = "Example User",
                        email = "user@example.com"
                ),
                description = "API documentation",
                title = "API Documentation",
                version = "1.0",
                license = @License(
                        name = "License Name",
                        url = "https://example.com"
                ),
                termsOfService = "Terms of Service"
        ),
        servers = {
                @Server(
                        description = "Local Environment",
                        url = "http://localhost:8080"
                )
        },
        security = {
                @SecurityRequirement(
                        name = "keycloak"
                )
        }
)
@SecurityScheme(
        name = "keycloak",
        type = SecuritySchemeType.OAUTH2,
        in = SecuritySchemeIn.HEADER,
        description = "Keycloak OAuth2 Authorization Code flow",
        flows = @OAuthFlows(
                authorizationCode = @OAuthFlow(
                        authorizationUrl = "http://localhost:8080/auth/realms/your-realm/protocol/openid-connect/auth",
                        tokenUrl = "http://localhost:8080/auth/realms/your-realm/protocol/openid-connect/token"
                )
        )
)
public class OpenApiConfig {
}
Client Credentials Flow

For server-to-server authentication using OAuth2 Client Credentials flow:

Update OpenApiConfig Class:

package com.example.config;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.models.security.OAuthFlow;
import io.swagger.v3.oas.models.security.OAuthFlows;

@OpenAPIDefinition(
        info = @Info(
                contact = @Contact(
                        name = "Example User",
                        email = "user@example.com"
                ),
                description = "API documentation",
                title = "API Documentation",
                version = "1.0",
                license = @License(
                        name = "License Name",
                        url = "https://example.com"
                ),
                termsOfService = "Terms of Service"
        ),
        servers = {
                @Server(
                        description = "Local Environment",
                        url = "http://localhost:8080"
                )
        },
        security = {
                @SecurityRequirement(
                        name = "keycloak"
                )
        }
)
@SecurityScheme(
        name = "keycloak",
        type = SecuritySchemeType.OAUTH2,
        in = SecuritySchemeIn.HEADER,
        description = "Keycloak OAuth2 Client Credentials flow",
        flows = @OAuthFlows(
                clientCredentials = @OAuthFlow(
                        tokenUrl = "http://localhost:8080/auth/realms/your-realm/protocol/openid-connect/token"
                )
        )
)
public class OpenApiConfig {
}

3. Customize Unauthorized Responses

To provide a custom error response for unauthorized access:

Implement Custom Error Handling:

package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Configuration
public class SecurityConfig {

    @Bean
    public ServerAuthenticationEntryPoint authenticationEntryPoint() {
        return (exchange, ex) -> {
            String expMessage = ex.getMessage() != null ? ex.getMessage() : "Unauthorized access - please provide valid credentials.";

            byte[] responseBytes = String.format("{\"status\": %d, \"message\": \"%s\"}", HttpStatus.UNAUTHORIZED.value(), expMessage)
                    .getBytes(StandardCharsets.UTF_8);

            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
            return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
                    .bufferFactory()
                    .wrap(responseBytes)));
        };
    }

    @Bean
    public ServerAccessDeniedHandler accessDeniedHandler() {
        return (exchange, denied) -> {
            String expMessage = denied.getMessage() != null ? denied.getMessage() : "Access Denied";

            byte[] responseBytes = String.format("{\"status\": %d, \"message\": \"%s\"}", HttpStatus.FORBIDDEN.value(), expMessage)
                    .getBytes(StandardCharsets.UTF_8);

            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
            return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
                    .bufferFactory()
                    .wrap(responseBytes)));
        };
    }
}

Conclusion

By following these steps, you can configure Swagger UI to use Bearer Authentication with Keycloak for both Authorization Code and Client Credentials flows. Additionally, customizing unauthorized responses helps provide clear, user-friendly error messages. This setup enhances the security and usability of your API documentation.

Feel free to adapt these instructions based on your specific requirements and Keycloak setup. If you have any questions or run into issues, don't hesitate to reach out for further assistance.

Happy coding!