feat: API key and HTTP basic authentication support#24
feat: API key and HTTP basic authentication support#24halotukozak wants to merge 2 commits intomasterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class OpenAPI security scheme support (API key + HTTP Basic, alongside Bearer/backward-compat) to the parsing and codegen pipeline, and wires Gradle plugin shared-type generation to reflect spec-defined authentication.
Changes:
- Introduces
SecurityScheme/ApiKeyLocationin the core model and parses globalsecuritySchemes+securityusage from OpenAPI. - Generates auth-aware
ApiClientBase.applyAuth()and security-aware client constructors (with backward-compat behavior for legacy Bearer-only usage). - Updates Gradle plugin to feed spec files into shared type generation; adds extensive unit + functional tests.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| plugin/src/main/kotlin/com/avsystem/justworks/gradle/JustworksSharedTypesTask.kt | Adds specFiles input and parses specs to drive shared ApiClientBase auth generation. |
| plugin/src/main/kotlin/com/avsystem/justworks/gradle/JustworksPlugin.kt | Wires each spec into the shared types task for security scheme extraction. |
| plugin/src/functionalTest/kotlin/com/avsystem/justworks/gradle/JustworksPluginFunctionalTest.kt | Functional coverage for security-aware vs backward-compatible ApiClientBase. |
| core/src/test/resources/security-schemes-spec.yaml | Fixture spec covering Bearer/apiKey/header+query/basic. |
| core/src/test/kotlin/com/avsystem/justworks/core/parser/SpecParserSecurityTest.kt | Unit tests for security scheme extraction/filtering. |
| core/src/test/kotlin/com/avsystem/justworks/core/gen/ClientGeneratorTest.kt | Constructor generation tests for various security scheme sets. |
| core/src/test/kotlin/com/avsystem/justworks/core/gen/ApiClientBaseGeneratorTest.kt | Tests for generated constructor params and applyAuth() body per scheme. |
| core/src/main/kotlin/com/avsystem/justworks/core/parser/SpecParser.kt | Extracts referenced security schemes from components + global security requirements. |
| core/src/main/kotlin/com/avsystem/justworks/core/model/ApiSpec.kt | Adds SecurityScheme model and ApiSpec.securitySchemes. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/Names.kt | Adds BASE64_CLASS for Basic auth generation. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/CodeGenerator.kt | Passes security schemes into shared-type generation. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/ClientGenerator.kt | Generates security-aware constructors and super-calls. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/ApiClientBaseGenerator.kt | Generates scheme-dependent auth params + dynamic applyAuth() implementation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (scheme in headerSchemes) { | ||
| when (scheme) { | ||
| is SecurityScheme.Bearer -> { | ||
| val paramName = if (isSingleBearer) TOKEN else "${scheme.name.toCamelCase()}Token" | ||
| builder.addStatement( | ||
| "append(%T.Authorization, %P)", | ||
| HTTP_HEADERS, | ||
| CodeBlock.of("Bearer \${$paramName()}"), | ||
| ) | ||
| } | ||
|
|
||
| is SecurityScheme.Basic -> { | ||
| val usernameParam = "${scheme.name.toCamelCase()}Username" | ||
| val passwordParam = "${scheme.name.toCamelCase()}Password" | ||
| builder.addStatement( | ||
| "append(%T.Authorization, %P)", | ||
| HTTP_HEADERS, | ||
| CodeBlock.of( | ||
| "Basic \${%T.getEncoder().encodeToString(\"${'$'}{$usernameParam()}:${'$'}{$passwordParam()}\".toByteArray())}", | ||
| BASE64_CLASS, | ||
| ), | ||
| ) |
There was a problem hiding this comment.
For multiple header-based schemes, this loop can append multiple Authorization header values (e.g., Bearer + Basic). Multiple Authorization headers are generally invalid/undefined behavior, and OpenAPI security objects are OR’ed across entries (and AND’ed within an entry), not "apply all schemes". Consider modeling/propagating the security requirements structure so callers can select which scheme to apply, or at least ensure only one Authorization header is emitted (e.g., choose one scheme deterministically or make it configurable).
| for (scheme in headerSchemes) { | |
| when (scheme) { | |
| is SecurityScheme.Bearer -> { | |
| val paramName = if (isSingleBearer) TOKEN else "${scheme.name.toCamelCase()}Token" | |
| builder.addStatement( | |
| "append(%T.Authorization, %P)", | |
| HTTP_HEADERS, | |
| CodeBlock.of("Bearer \${$paramName()}"), | |
| ) | |
| } | |
| is SecurityScheme.Basic -> { | |
| val usernameParam = "${scheme.name.toCamelCase()}Username" | |
| val passwordParam = "${scheme.name.toCamelCase()}Password" | |
| builder.addStatement( | |
| "append(%T.Authorization, %P)", | |
| HTTP_HEADERS, | |
| CodeBlock.of( | |
| "Basic \${%T.getEncoder().encodeToString(\"${'$'}{$usernameParam()}:${'$'}{$passwordParam()}\".toByteArray())}", | |
| BASE64_CLASS, | |
| ), | |
| ) | |
| var authorizationHeaderAdded = false | |
| for (scheme in headerSchemes) { | |
| when (scheme) { | |
| is SecurityScheme.Bearer -> { | |
| if (!authorizationHeaderAdded) { | |
| val paramName = if (isSingleBearer) TOKEN else "${scheme.name.toCamelCase()}Token" | |
| builder.addStatement( | |
| "append(%T.Authorization, %P)", | |
| HTTP_HEADERS, | |
| CodeBlock.of("Bearer \${$paramName()}"), | |
| ) | |
| authorizationHeaderAdded = true | |
| } | |
| } | |
| is SecurityScheme.Basic -> { | |
| if (!authorizationHeaderAdded) { | |
| val usernameParam = "${scheme.name.toCamelCase()}Username" | |
| val passwordParam = "${scheme.name.toCamelCase()}Password" | |
| builder.addStatement( | |
| "append(%T.Authorization, %P)", | |
| HTTP_HEADERS, | |
| CodeBlock.of( | |
| "Basic \${%T.getEncoder().encodeToString(\"${'$'}{$usernameParam()}:${'$'}{$passwordParam()}\".toByteArray())}", | |
| BASE64_CLASS, | |
| ), | |
| ) | |
| authorizationHeaderAdded = true | |
| } |
| // Wire spec file into shared types task for security scheme extraction | ||
| sharedTypesTask.configure { task -> | ||
| task.specFiles.from(spec.specFile) | ||
| } |
There was a problem hiding this comment.
Wiring every spec file into a single shared-types task means ApiClientBase will be generated from the union of security schemes across all specs. However, ClientGenerator generates each client constructor/super-call from that spec’s own securitySchemes only, so in a multi-spec project with different auth schemes the generated clients can fail to compile (super constructor parameter mismatch, and even token vs <scheme>NameToken naming differences when the base sees multiple schemes). To avoid this, consider either (a) keeping ApiClientBase auth constructor stable and generating scheme-specific auth in each client, (b) generating ApiClientBase per spec instead of shared, or (c) making the shared ApiClientBase constructor accept optional/named auth providers with defaults and having clients call super with named args (or ensure all clients are generated against the same union list).
| private fun extractSecuritySchemes(): List<SecurityScheme> = specFiles.files | ||
| .mapNotNull { file -> | ||
| when (val result = SpecParser.parse(file)) { | ||
| is ParseResult.Success -> result.apiSpec.securitySchemes | ||
| is ParseResult.Failure -> { | ||
| logger.warn("Failed to parse spec '${file.name}': ${result.errors.joinToString()}") | ||
| null | ||
| } | ||
| } | ||
| } | ||
| .flatten() | ||
| .distinctBy { it.name } |
There was a problem hiding this comment.
specFiles.files iteration order is not guaranteed to be stable, and .distinctBy { it.name } will keep the first occurrence. This can make generated ApiClientBase nondeterministic across machines/runs (constructor param order / applyAuth body) and can silently mask conflicting scheme definitions across multiple specs with the same name. Consider sorting the input files and/or the resulting schemes (e.g., by scheme name) for deterministic generation, and emitting a warning/error if the same scheme name is encountered with different configuration.
| private fun extractSecuritySchemes(): List<SecurityScheme> = specFiles.files | |
| .mapNotNull { file -> | |
| when (val result = SpecParser.parse(file)) { | |
| is ParseResult.Success -> result.apiSpec.securitySchemes | |
| is ParseResult.Failure -> { | |
| logger.warn("Failed to parse spec '${file.name}': ${result.errors.joinToString()}") | |
| null | |
| } | |
| } | |
| } | |
| .flatten() | |
| .distinctBy { it.name } | |
| private fun extractSecuritySchemes(): List<SecurityScheme> { | |
| // Parse specs in a deterministic order (by file path) so that any tie‑breaking | |
| // between schemes with the same name is stable across machines/runs. | |
| val schemesWithSource: List<Pair<java.io.File, SecurityScheme>> = specFiles.files | |
| .sortedBy { it.path } | |
| .flatMap { file -> | |
| when (val result = SpecParser.parse(file)) { | |
| is ParseResult.Success -> result.apiSpec.securitySchemes.map { scheme -> file to scheme } | |
| is ParseResult.Failure -> { | |
| logger.warn("Failed to parse spec '${file.name}': ${result.errors.joinToString()}") | |
| emptyList() | |
| } | |
| } | |
| } | |
| // Group by scheme name and resolve duplicates deterministically, warning on conflicts. | |
| val groupedByName: Map<String, List<Pair<java.io.File, SecurityScheme>>> = | |
| schemesWithSource.groupBy { (_, scheme) -> scheme.name } | |
| val resolvedSchemes = mutableListOf<SecurityScheme>() | |
| for ((name, entries) in groupedByName.toSortedMap()) { | |
| if (entries.isEmpty()) continue | |
| val schemes = entries.map { it.second } | |
| val firstScheme = schemes.first() | |
| // Detect conflicting definitions (same name, different configuration). | |
| val hasConflict = schemes.any { it != firstScheme } | |
| if (hasConflict) { | |
| val files = entries.map { it.first.name }.distinct().sorted() | |
| logger.warn( | |
| "Conflicting security scheme definitions for '$name' across specs " + | |
| "(${files.joinToString(", ")}). Using definition from '${entries.first().first.name}'." | |
| ) | |
| } | |
| resolvedSchemes += firstScheme | |
| } | |
| return resolvedSchemes | |
| } |
Coverage Report
|
…arnings Replace SpecValidator.ValidationIssue and string-based error lists with typed Issue.Error/Issue.Warning and Arrow's IorRaise for non-short-circuiting warning accumulation. ParseResult now carries List<Issue.Warning> and a single Issue.Error on failure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion Parse security schemes (Bearer, Basic, ApiKey) from OpenAPI specs and generate auth-aware ApiClientBase with corresponding constructor parameters and header/query injection. Wire spec files into JustworksSharedTypesTask for security scheme extraction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0c0cb14 to
2cd275d
Compare
Summary
SecuritySchemesealed interface model (Bearer, ApiKey with header/query, Basic)SpecParserto extractsecuritySchemesfrom OpenAPI spec and resolve globalsecurityreferencesapplyAuth()generation inApiClientBaseGenerator— conditional logic per scheme typeClientGenerator— backward-compatible (tokenfor single Bearer)JustworksSharedTypesTaskfor end-to-end security scheme generationTest plan
SpecParserSecurityTest— 8 tests for scheme extraction (bearer, apiKey header/query, basic, filtering)ApiClientBaseGeneratorTest— 24 tests including dynamic applyAuth for all scheme typesClientGeneratorTest— 28 tests including security-aware constructorsJustworksPluginFunctionalTest— 2 functional tests for on-disk ApiClientBase with security🤖 Generated with Claude Code