Skip to content

Add TrustStore Support for Fetching OpenAPI via HTTPS with Self-Signed Certificates #136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 33 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ Gradle Groovy DSL

```groovy
plugins {
id "org.springframework.boot" version "2.7.0"
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
id "org.springframework.boot" version "2.7.0"
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
}
```

Expand Down Expand Up @@ -73,24 +73,30 @@ openApi as follows

```kotlin
openApi {
apiDocsUrl.set("https://localhost:9000/api/docs")
outputDir.set(file("$buildDir/docs"))
outputFileName.set("swagger.json")
waitTimeInSeconds.set(10)
groupedApiMappings.set(["https://localhost:8080/v3/api-docs/groupA" to "swagger-groupA.json",
"https://localhost:8080/v3/api-docs/groupB" to "swagger-groupB.json"])
customBootRun {
args.set(["--spring.profiles.active=special"])
}
apiDocsUrl.set("https://localhost:9000/api/docs")
outputDir.set(file("$buildDir/docs"))
outputFileName.set("swagger.json")
waitTimeInSeconds.set(10)
trustStore.set("keystore/truststore.p12")
trustStorePassword.set("changeit".toCharArray())
groupedApiMappings.set(
["https://localhost:8080/v3/api-docs/groupA" to "swagger-groupA.json",
"https://localhost:8080/v3/api-docs/groupB" to "swagger-groupB.json"]
)
customBootRun {
args.set(["--spring.profiles.active=special"])
}
}
```

| Parameter | Description | Required | Default |
|----------------------|-------------------------------------------------------------------------------------------------------------------------------------|----------|--------------------------------------|
| `apiDocsUrl` | The URL from where the OpenAPI doc can be downloaded. If the url ends with `.yaml`, output will YAML format. | No | http://localhost:8080/v3/api-docs |
| `apiDocsUrl` | The URL from where the OpenAPI doc can be downloaded. If the url ends with `.yaml`, output will YAML format. | No | http://localhost:8080/v3/api-docs |
| `outputDir` | The output directory for the generated OpenAPI file | No | $buildDir - Your project's build dir |
| `outputFileName` | Specifies the output file name. | No | openapi.json |
| `outputFileName` | Specifies the output file name. | No | openapi.json |
| `waitTimeInSeconds` | Time to wait in seconds for your Spring Boot application to start, before we make calls to `apiDocsUrl` to download the OpenAPI doc | No | 30 seconds |
| `trustStore` | Path to a trust store that contains custom trusted certificates. | No | `<None>` |
| `trustStorePassword` | Password to open Trust Store | No | `<None>` |
| `groupedApiMappings` | A map of URLs (from where the OpenAPI docs can be downloaded) to output file names | No | [] |
| `customBootRun` | Any bootRun property that you would normal need to start your spring boot application. | No | (N/A) |

Expand Down Expand Up @@ -134,6 +140,20 @@ openApi {
}
```

### Trust Store Configuration

If you have restricted your application to HTTPS only and prefer not to include your certificate
in Java's cacerts file, you can configure your own set of trusted certificates through plugin
properties, ensuring SSL connections are established.

#### Generating a Trust Store

To create your own Trust Store, utilize the Java keytool command:

```shell
keytool -storepass changeit -noprompt -import -alias ca -file [CERT_PATH]/ca.crt -keystore [KEYSTORE_PATH]/truststore.p12 -deststoretype PKCS12
```

### Grouped API Mappings Notes

The `groupedApiMappings` customization allows you to specify multiple URLs/file names for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ open class OpenApiExtension @Inject constructor(
val outputFileName: Property<String> = objects.property(String::class.java)
val outputDir: DirectoryProperty = objects.directoryProperty()
val waitTimeInSeconds: Property<Int> = objects.property(Int::class.java)
val trustStore: Property<String> = objects.property(String::class.java)
val trustStorePassword: Property<CharArray> = objects.property(CharArray::class.java)

val groupedApiMappings: MapProperty<String, String> =
objects.mapProperty(String::class.java, String::class.java)
val customBootRun: CustomBootRunAction =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@ import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import java.io.FileInputStream
import java.net.ConnectException
import java.net.HttpURLConnection
import java.net.URL
import java.security.KeyStore
import java.security.SecureRandom
import java.time.Duration
import java.time.temporal.ChronoUnit.SECONDS
import java.util.Locale
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.KeyManager
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory


private const val MAX_HTTP_STATUS_CODE = 299

Expand All @@ -40,9 +50,17 @@ open class OpenApiGeneratorTask : DefaultTask() {

@get:OutputDirectory
val outputDir: DirectoryProperty = project.objects.directoryProperty()

@get:Internal
val waitTimeInSeconds: Property<Int> =
project.objects.property(Int::class.java)
val waitTimeInSeconds: Property<Int> = project.objects.property(Int::class.java)

@get:Optional
@get:Input
val trustStore: Property<String> = project.objects.property(String::class.java)

@get:Optional
@get:Input
val trustStorePassword: Property<CharArray> = project.objects.property(CharArray::class.java)

init {
description = OPEN_API_TASK_DESCRIPTION
Expand All @@ -56,6 +74,8 @@ open class OpenApiGeneratorTask : DefaultTask() {
groupedApiMappings.convention(extension.groupedApiMappings)
outputDir.convention(extension.outputDir)
waitTimeInSeconds.convention(extension.waitTimeInSeconds)
trustStore.convention(extension.trustStore)
trustStorePassword.convention(extension.trustStorePassword)
}

@TaskAction
Expand All @@ -69,20 +89,23 @@ open class OpenApiGeneratorTask : DefaultTask() {

private fun generateApiDocs(url: String, fileName: String) {
try {
val isYaml = url.toLowerCase().matches(Regex(".+[./]yaml(/.+)*"))
val isYaml = url.lowercase(Locale.getDefault()).matches(Regex(".+[./]yaml(/.+)*"))
val sslContext = getCustomSslContext()
await ignoreException ConnectException::class withPollInterval Durations.ONE_SECOND atMost Duration.of(
waitTimeInSeconds.get().toLong(),
SECONDS
) until {
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
val connection: HttpURLConnection =
URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.connect()
val statusCode = connection.responseCode
logger.trace("apiDocsUrl = {} status code = {}", url, statusCode)
logger.debug("apiDocsUrl = {} status code = {}", url, statusCode)
statusCode < MAX_HTTP_STATUS_CODE
}
logger.info("Generating OpenApi Docs..")
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
val connection: HttpURLConnection =
URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"
Expand All @@ -103,6 +126,24 @@ open class OpenApiGeneratorTask : DefaultTask() {
}
}

private fun getCustomSslContext(): SSLContext {
if (trustStore.isPresent) {
logger.debug("Reading truststore: ${trustStore.get()}")
FileInputStream(trustStore.get()).use { truststoreFile ->
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
val truststore = KeyStore.getInstance(KeyStore.getDefaultType())
truststore.load(truststoreFile, trustStorePassword.get())
trustManagerFactory.init(truststore)
val sslContext: SSLContext = SSLContext.getInstance("TLSv1.2")
val keyManagers = arrayOf<KeyManager>()
sslContext.init(keyManagers, trustManagerFactory.trustManagers, SecureRandom())

return sslContext
}
}
return SSLContext.getDefault()
}

private fun prettifyJson(response: String): String {
val gson = GsonBuilder().setPrettyPrinting().create()
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,27 @@ class OpenApiGradlePluginTest {
assertOpenApiJsonFile(1)
}

@Test
fun `using HTTPS api url to download api-docs`() {
val trustStore = File(projectTestDir, "truststore.p12")
buildFile.writeText(
"""$baseBuildGradle

openApi{
trustStore = "${trustStore.absolutePath}"
trustStorePassword = "changeit".toCharArray()
apiDocsUrl = "https://127.0.0.1:8081/v3/api-docs"
customBootRun {
args = ["--spring.profiles.active=ssl"]
}
}
""".trimMargin()
)

assertEquals(TaskOutcome.SUCCESS, openApiDocsTask(runTheBuild()).outcome)
assertOpenApiJsonFile(1)
}

@Test
fun `yaml generation`() {
val outputYamlFileName = "openapi.yaml"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
server.port=8081
server.ssl.key-alias=ssl
server.ssl.key-password=+bAyoiVYOy6Tg/v2IG4blme2Hu+ORTksvFh/w9s=
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=+bAyoiVYOy6Tg/v2IG4blme2Hu+ORTksvFh/w9s=
server.ssl.key-store-type=PKCS12
Binary file not shown.
Binary file not shown.