Skip to content

feat: API key and HTTP basic authentication support#24

Open
halotukozak wants to merge 2 commits intomasterfrom
feat/security-schemes
Open

feat: API key and HTTP basic authentication support#24
halotukozak wants to merge 2 commits intomasterfrom
feat/security-schemes

Conversation

@halotukozak
Copy link
Member

Summary

  • Add SecurityScheme sealed interface model (Bearer, ApiKey with header/query, Basic)
  • Extend SpecParser to extract securitySchemes from OpenAPI spec and resolve global security references
  • Dynamic applyAuth() generation in ApiClientBaseGenerator — conditional logic per scheme type
  • Security-aware constructor params in ClientGenerator — backward-compatible (token for single Bearer)
  • Wire spec files through JustworksSharedTypesTask for end-to-end security scheme generation

Test plan

  • SpecParserSecurityTest — 8 tests for scheme extraction (bearer, apiKey header/query, basic, filtering)
  • ApiClientBaseGeneratorTest — 24 tests including dynamic applyAuth for all scheme types
  • ClientGeneratorTest — 28 tests including security-aware constructors
  • JustworksPluginFunctionalTest — 2 functional tests for on-disk ApiClientBase with security
  • Backward compatibility: specs without security produce unchanged output

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 23, 2026 10:42
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / ApiKeyLocation in the core model and parses global securitySchemes + security usage 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.

Comment on lines +246 to +267
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,
),
)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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
}

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +66
// Wire spec file into shared types task for security scheme extraction
sharedTypesTask.configure { task ->
task.specFiles.from(spec.specFile)
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +58
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 }
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
}

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

github-actions bot commented Mar 24, 2026

Coverage Report

Overall Project 94.8% -1.01% 🍏
Files changed 91.1% 🍏

File Coverage
Names.kt 100% 🍏
ApiClientBaseGenerator.kt 100% 🍏
ClientGenerator.kt 99.76% 🍏
ApiSpec.kt 99.36% 🍏
Issue.kt 93.33% -6.67% 🍏
SpecParser.kt 92.96% -1.63% 🍏
SpecValidator.kt 76.14% -23.86%
CodeGenerator.kt 0% -26.73%

halotukozak and others added 2 commits March 25, 2026 11:01
…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>
@halotukozak halotukozak force-pushed the feat/security-schemes branch from 0c0cb14 to 2cd275d Compare March 25, 2026 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants