You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+197Lines changed: 197 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -305,6 +305,203 @@ What the example demonstrates:
305
305
-**One decorator stamps a response header.**`Response.withHeader(...)` is non-destructive — the handler's `Response` is replaced with one that has the extra header.
306
306
-**Handler is a pure function.** Reads from `Request`, returns a `Response` value. No `HttpExchange`, no try/catch IOException, no builder.
The library parses `components.securitySchemes` and the `security` requirement lists (root-level and per-operation), extracts the credential per scheme, hands it to a consumer-provided `SchemeValidator` callback, and renders RFC 7807 `application/problem+json` rejections — 401 for missing/malformed credentials (with `WWW-Authenticate`), 403 when the validator denies.
311
+
312
+
Supported scheme types in this release:
313
+
314
+
-`apiKey` (in `header`, `query`, or `cookie`)
315
+
-`http``bearer`
316
+
-`http``basic`
317
+
318
+
`oauth2`, `openIdConnect`, and `mutualTLS` are parsed into a placeholder type (`SecurityScheme.Unsupported`) — if any operation actually *references* one of those scheme names, the server fails at boot.
319
+
320
+
#### Declaring schemes in the spec
321
+
322
+
```yaml
323
+
components:
324
+
securitySchemes:
325
+
apiKeyAuth:
326
+
type: apiKey
327
+
name: X-API-Key
328
+
in: header
329
+
bearerAuth:
330
+
type: http
331
+
scheme: bearer
332
+
basicAuth:
333
+
type: http
334
+
scheme: basic
335
+
336
+
# Either default for every operation:
337
+
security:
338
+
- bearerAuth: []
339
+
340
+
# Or attach per-operation (overrides the root default):
341
+
paths:
342
+
/reports/{id}:
343
+
get:
344
+
operationId: getReport
345
+
security:
346
+
- apiKeyAuth: []
347
+
responses:
348
+
"200": { description: ok }
349
+
```
350
+
351
+
`security: []` on an operation means "no security required" (overrides the root default). Omitting `security` on an operation inherits the root default.
352
+
353
+
When several entries appear in `security`, they are OR-ed; the request is allowed if *any* entry's schemes all validate. Multiple keys *inside* one entry are AND-ed:
354
+
355
+
```yaml
356
+
security:
357
+
# Either an API key …
358
+
- apiKeyAuth: []
359
+
# … or BOTH a bearer token AND a tenant header validator:
BasicCredential bc = (BasicCredential) credential;
387
+
return userService
388
+
.authenticate(bc.username(), bc.password())
389
+
.map(user -> user); // Optional<User>
390
+
})
391
+
.build();
392
+
```
393
+
394
+
The library guarantees the `Credential` variant matches the scheme's declared type — `apiKey` schemes deliver `ApiKeyCredential`, `http` `bearer` delivers `BearerCredential`, `http` `basic` delivers `BasicCredential`. Pattern matching is cleaner than casts:
case ApiKeyCredential ak -> apiKeyStore.lookup(ak.value()).map(user -> user);
399
+
case BearerCredential b -> jwt.verify(b.token()).map(claims -> claims);
400
+
case BasicCredential bc -> userService.authenticate(bc.username(), bc.password()).map(u -> u);
401
+
})
402
+
```
403
+
404
+
#### Constructing the principal
405
+
406
+
A *principal* is whatever the library hands back to the handler after a successful authentication. The library does NOT define a `Principal` type — your validator returns `Optional<Object>` and the library stashes the value on the `Request` under the scheme name. **Whatever you return becomes your principal.**
407
+
408
+
Three common patterns:
409
+
410
+
**1. A domain record.** Best for typed access in handlers.
411
+
412
+
```java
413
+
public record AuthenticatedUser(String userId, String tenantId, Set<String> roles) {}
Returning `Optional.empty()` from a validator means "deny" — the library then returns 403 Forbidden (or 401 if no scheme produced a valid credential at all). Throwing from a validator propagates to the configured `ExceptionHandler`; it does NOT count as deny, so let your validators throw on internal errors and return `Optional.empty()` only when the credential is genuinely invalid.
462
+
463
+
#### Boot-time validation
464
+
465
+
If `security` references a scheme that has no registered `securityValidator(...)`, is undeclared in `components.securitySchemes`, or uses an unsupported type, `OpenApiServer.builder()...build()` throws `IllegalStateException` immediately. You can't ship a server that's missing an auth check by accident — the failure is loud at startup, not silent at request time.
466
+
467
+
#### Opt-out: external authentication
468
+
469
+
In some deployments authentication happens upstream — for example, an Envoy sidecar with OPA, or an API Gateway like Apigee that already verified the credential before the request reaches your JVM. In that case the credential never arrives in a form the library can validate (or the library would be re-validating something the gateway already proved), and forcing you to register stub validators is just friction.
470
+
471
+
`useExternalAuthentication()` opts the entire library out of in-process enforcement:
472
+
473
+
```java
474
+
OpenApiServer.builder()
475
+
.spec(spec)
476
+
.handlers(handlers)
477
+
.useExternalAuthentication() // SecurityFilter becomes a no-op
478
+
.build();
479
+
```
480
+
481
+
Effects when set:
482
+
483
+
- `SecurityFilter`short-circuits to the next chain step regardless of any `security` declarations — every request reaches the handler.
484
+
- The boot-time validator-registration check is skipped, so you don't have to register `.securityValidator(...)` callbacks at all.
485
+
- `Request.principals()`returns an empty map; `Request.principal(name)` returns `Optional.empty()`. **The library never reads sidecar-set headers.** If you want a principal in the handler, write a normal `RequestInterceptor` that reads whatever header the sidecar sets and binds a `ScopedValue` (or stashes on the request via a domain wrapper of your own).
The library still parses `components.securitySchemes` and exposes it via `spec.securitySchemes()` — useful if you serve the OpenAPI document or wire a docs UI — it just stops short of *enforcing* anything.
504
+
308
505
### Request body content types
309
506
310
507
The server reads `requestBody.content` from the spec and selects a mapper by the request's media type (the bare `type/subtype` from `Content-Type`, e.g. `application/json`; lookup is case-insensitive):
0 commit comments