When working as a backend engineer, it is essential to document the REST APIs. It also helps in providing a UI(swagger-ui) to test the REST calls. Let us try to integrate springdoc-openapi to provide swagger documentation for a spring boot project using spring-security(OAuth2).

Add the dependencies to build.gradle

1
2
3
implementation("org.springdoc:springdoc-openapi-ui:1.6.8")
implementation("org.springdoc:springdoc-openapi-security:1.6.8")
implementation("org.springdoc:springdoc-openapi-kotlin:1.6.8")

Kotlin code example

The following example showcases spring configuration to create OpenAPI bean which supports two authentication mechanism:

  1. OAuth2 ClientCredentials flow
  2. Bearer Token
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Contact
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.security.OAuthFlow
import io.swagger.v3.oas.models.security.OAuthFlows
import io.swagger.v3.oas.models.security.SecurityRequirement
import io.swagger.v3.oas.models.security.SecurityScheme
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile


@Configuration
class OpenApiConfig {
    companion object {
        private const val OAUTH2_CLIENT_CREDENTIALS_KEY = "oauth2ClientCredentials"
        private const val BEARER_TOKEN_KEY = "bearerToken"
    }
    @Value("\${springdoc.version}")
    private lateinit var appVersion: String

    @Value("\${spring.application.name}")
    private lateinit var applicationName: String

    @Value("\${springdoc.oAuthFlow.authorizationUrl}")
    private lateinit var authorizationUrl: String

    @Value("\${springdoc.oAuthFlow.tokenUrl}")
    private lateinit var tokenUrl: String

    @Bean
    fun customOpenAPI(): OpenAPI {
        return OpenAPI()
            .info(
                Info()
                    .title(applicationName)
                    .version(appVersion)
                    .contact(Contact().name("Team").email("test@test.com"))
                    .termsOfService("https://springdoc.org/")
            )
            .components(
                Components()
                    .addSecuritySchemes(OAUTH2_CLIENT_CREDENTIALS_KEY, clientCredentialSecurityScheme())
                    .addSecuritySchemes(BEARER_TOKEN_KEY, bearerTokenSecurityScheme())
            )
            .addSecurityItem(SecurityRequirement().addList(OAUTH2_CLIENT_CREDENTIALS_KEY))
            .addSecurityItem(SecurityRequirement().addList(BEARER_TOKEN_KEY))
    }

    private fun bearerTokenSecurityScheme() =
        SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT")

    private fun clientCredentialSecurityScheme() = SecurityScheme().type(SecurityScheme.Type.OAUTH2).flows(
        OAuthFlows().clientCredentials(OAuthFlow().authorizationUrl(authorizationUrl).tokenUrl(tokenUrl))
    )
}

In the above example no scopes have been configured. Lets add read and write scopes:

1
2
3
4
5
 private fun clientCredentialSecurityScheme() = SecurityScheme().type(SecurityScheme.Type.OAUTH2).flows(
     OAuthFlows().clientCredentials(OAuthFlow().authorizationUrl(authorizationUrl).tokenUrl(tokenUrl).scopes(readWriteScope()))
 )

 private fun readWriteScope() = Scopes().addString("read", "Can Read").addString("write", "Can Write")

Note that springdoc also supports other authentication mechanism like basic auth, implicit grant, etc.

When using spring-security, dont forget to whitelist following urls:

  • “/v3/api-docs/**”
  • “/swagger-ui/**”
  • “/swagger-ui.html”

Spring application configuration

Let’s configure spring’s application.yaml with authorization and token urls

1
2
3
4
5
springdoc:
  version: v1
  oAuthFlow:
    authorizationUrl: https://dev-88823233.okta.com/oauth2/default/v1/authorize
    tokenUrl: https://dev-88823233.okta.com/oauth2/default/v1/token

Note that the okta urls don’t exist anymore 😁. But you may replace them with your authorization and token url.

Usually these urls are exposed at well known endpoints by your Authorization Server

  • oidc provider: /.well-known/openid-configuration url
  • oauth2: okta provides the url configurations under https://dev-88823233.okta.com/oauth2/default/.well-known/oauth-authorization-server

Sometimes, it might be useful to disable swagger in production.

1
2
3
4
5
springdoc:
  api-docs:
    enabled: false
  swagger-ui:
    enabled: false

Swagger of Swaggers

When working with multiple services, we should ideally have a single swagger ui which could act as a swagger aggregate to host all the swagger docs under a single service. Lets say we have following two services:

  • user service: Java based spring boot service & springdoc exposing only swagger api docs at https://ravikrs.com/users/v3/api-docs.
1
2
3
4
5
springdoc:
  api-docs:
    enabled: true
  swagger-ui:
    enabled: false

ui would be used from aggregate swagger service, so its disabled in user service.

  • api service: Go service using swaggo for exposing swagger api docs at https://ravikrs.com/api/swagger/doc.json

We may combine multiple such services inside a single swagger service(https://ravikrs.com/swagger/swagger-ui.html) which could be a simple spring boot service aggregating all swagger api docs and exposing in a single Swagger UI.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
springdoc:
  version: v1
  api-docs:
    enabled: true
  swagger-ui:
    enabled: true
    urls:
      - name: Api
        url: https://ravikrs.com/api/swagger/doc.json
      - name: User
        url: https://ravikrs.com/user/v3/api-docs

CORS issue when using multiple domain for swagger

When working with multiple domains, we may need to enable CORS for accessing /v3/api-docs url to retrieve openapi3 docs. Following code snippet showd how to declare a cors filter for spring apps.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Bean
public CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    source.registerCorsConfiguration("/v3/api-docs", config);
    return new CorsFilter(source);
}