Skip to content

[jaxrs-spec][quarkus] Emit @RolesAllowed({scope}) for OAuth2 and OpenID Connect operations with explicit scopes#23752

Draft
Ignacio-Vidal wants to merge 19 commits intoOpenAPITools:masterfrom
Ignacio-Vidal:quarkus-authorisation
Draft

[jaxrs-spec][quarkus] Emit @RolesAllowed({scope}) for OAuth2 and OpenID Connect operations with explicit scopes#23752
Ignacio-Vidal wants to merge 19 commits intoOpenAPITools:masterfrom
Ignacio-Vidal:quarkus-authorisation

Conversation

@Ignacio-Vidal
Copy link
Copy Markdown
Contributor

@Ignacio-Vidal Ignacio-Vidal commented May 10, 2026

This is part 3 of #23691 to improve authentication and authorisation support in the jaxrs-spec/quarkus server generator, building on #23680.

What this PR does

Extends the useJakartaSecurityAnnotations flag (introduced in #23680) to also emit @jakarta.annotation.security.RolesAllowed({"scope1","scope2"}) on JAX-RS interface methods and implementation stubs for operations whose OAuth2 or OpenID Connect security requirements carry explicit scopes.

PR #23680 covered the any-authenticated-user case (@RolesAllowed({"**"})). This PR covers the scoped-role case: when every OR alternative has at least one scope, the generator emits a single @RolesAllowed whose value is the deduplicated, alphabetically sorted union of all scopes across those alternatives.

The wildcard and scoped emissions are mutually exclusive per operation: if any OR alternative qualifies as "any authenticated user", the wildcard wins and no scoped annotation is emitted on that operation.

Semantic rules

When @RolesAllowed({scope}) is emitted

Condition Result
All OR alternatives are OAuth2/OIDC with non-empty scopes @RolesAllowed({union_of_all_scopes})
Any OR alternative is unscoped (HTTP, API key, mTLS, or empty-scope OAuth2/OIDC) @RolesAllowed({"**"}) wins — PR #23680 path
Any OR alternative is anonymous (- {}) No annotation — deferred to @PermitAll (next PR)
AND group has more than one scheme with competing scopes No annotation + WARN log

OR scope union

When all OR alternatives are scoped, their scope lists are unioned, deduplicated, and sorted alphabetically:

security:
  - oauth2: [admin]   # OR
  - oauth2: [user]    # OR
# emits: @RolesAllowed({"admin","user"})

AND group — single scoped scheme

A single SecurityRequirement map with multiple keys is an AND group. The generator emits scoped roles only when at most one scheme in the AND group carries scopes. Unscoped co-required schemes (HTTP, API key, mTLS) contribute nothing to the scope list:

security:
  - apiKey: []          # AND — no scopes
    oauth2: [admin]     # AND — scoped
# emits: @RolesAllowed({"admin"})

If two or more schemes in the same AND group carry competing scope sets, Quarkus cannot enforce AND-of-different-scopes with a single annotation, so no annotation is emitted and a WARN is logged.

Global vs per-operation security

Operations with no security field inherit openapi.security. Operations with an explicit security: [] override produce no annotation regardless of the global block. A global security: [] block (top-level empty list) also suppresses emission on inheriting operations.

Why scoped and wildcard are mutually exclusive

Emitting both @RolesAllowed({"**"}) and @RolesAllowed({"admin"}) on the same method would cause Quarkus to apply both interceptors with AND semantics — net effect is the stricter annotation alone, which contradicts the OR intent of the spec. The processor short-circuits in the wildcard branch and never evaluates the scoped path for the same operation.

Implementation notes

Single vendor extension. The processor sets a single x-jakarta-roles-allowed vendor extension whose value is a List<String>, and collapses what was previously two mutually-exclusive flags (x-jakarta-any-roles boolean and x-jakarta-roles-allowed list) into one

  • ["**"] for the wildcard case (any-authenticated-user).
  • The sorted, deduplicated union of scope names for the scoped case.
  • Unset when the operation does not qualify (anonymous OR alternative, mixed-scope AND group, etc.).

Processor changes in JakartaSecurityAnnotationProcessor:

No changes to JavaJAXRSSpecServerCodegen. The existing fromOperation override from #23680 already delegates to the processor.

Template change in jakartaSecurityAnnotations.mustache:

{{#vendorExtensions.x-jakarta-roles-allowed.0}}
@jakarta.annotation.security.RolesAllowed({{openbrace}}{{#vendorExtensions.x-jakarta-roles-allowed}}"{{.}}"{{^-last}},{{/-last}}{{/vendorExtensions.x-jakarta-roles-allowed}}{{closebrace}})
{{/vendorExtensions.x-jakarta-roles-allowed.0}}
  • Single section guarded by .0 (the list's first element being truthy) to emit the annotation exactly once.
  • The inner section iterates the list to produce comma-separated quoted scope names.
  • {{openbrace}} / {{closebrace}} (globally defined in DefaultCodegen) avoid Mustache parser ambiguity when { immediately follows a tag close.
  • The partial has no trailing newline to satisfy the Mustache standalone-partial rule — otherwise the call-site's leading whitespace was being prepended to a phantom output line, doubling the indentation of the line that follows the partial in apiInterface.mustache and apiMethod.mustache.

New CI sample bin/configs/jaxrs-spec-quarkus-security.yaml exercises all four annotation paths (security: [], empty-scope, single-scope, OR-union-scope) from a single spec fixture. The generated sample is committed under samples/server/petstore/jaxrs-spec/quarkus-security/.

Test coverage

A new parameterised test (quarkusEmitsExpectedScopedRolesAllowedCount) drives a 16-row @DataProvider covering:

Scenario Fixture
OAuth2 single scope — two operations quarkus-oauth2-single-scope.yaml
OAuth2 multiple scopes — alphabetical order quarkus-oauth2-multiple-scopes.yaml
OAuth2 OR different scopes — union quarkus-oauth2-or-different-scopes.yaml
OAuth2 existing fixture — now emits scoped (was 0) quarkus-oauth2-with-scopes.yaml
OpenID Connect scoped — existing fixture quarkus-openidconnect-with-scopes.yaml
Global scoped security — both ops inherit quarkus-global-with-scopes.yaml
Global scoped + per-op override + per-op disable quarkus-global-scopes-op-override.yaml
AND group: API key + scoped OAuth2 (single scoped scheme) quarkus-and-with-apikey-and-scoped-oauth2.yaml
AND group: two competing scoped schemes — no emission quarkus-and-multi-scoped-warns.yaml
Unscoped OR scoped — wildcard wins on the unscoped op, scoped on the scoped-only op quarkus-oauth2-or-empty-and-scoped.yaml
Anonymous OR scoped — no emission quarkus-or-empty-anonymous-with-scopes.yaml

The data provider exercises both interfaceOnly=true (apiInterface.mustache) and interfaceOnly=false (apiMethod.mustache) for the most representative fixtures, so regressions in either template path are caught.

Additional targeted tests verify scope content and ordering (quarkusEmitsAlphabeticallySortedDeduplicatedScopes, quarkusOrAlternativesProduceUnionedScopes, quarkusGlobalScopesAreInheritedByOperationsWithoutOverride, quarkusOperationOverrideShadowsGlobalScopedSecurity), MicroProfile coexistence (quarkusScopedJakartaCoexistsWithMicroProfileAnnotations), mutual exclusion (quarkusNeverEmitsBothWildcardAndScopedRolesAllowedOnSameOperation), global empty-security inheritance (quarkusGlobalEmptySecurityListEmitsNothing), and end-to-end validation of the CI sample's four endpoints (quarkusMixedSecuritySampleEmitsAllExpectedAnnotations). All 32 existing PR #23680 test rows continue to pass unchanged.

Closes #23691 (partial — @PermitAll for anonymous/unannotated operations remains for the next PR).

@wing328 @jmini @MelleD

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • File the PR against the correct branch: master (upcoming 7.x.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR solves a reported issue, reference it using GitHub's linking syntax (e.g., having "fixes #123" present in the PR description)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Summary by cubic

Adds Jakarta security annotations to Quarkus JAX‑RS server stubs, emitting @jakarta.annotation.security.RolesAllowed({"**"}) when an operation requires "any authenticated user." Introduces useJakartaSecurityAnnotations (requires useJakartaEe=true) to align runtime security with OpenAPI.

  • New Features

    • New useJakartaSecurityAnnotations option for quarkus library; validates useJakartaEe=true.
    • Emits @jakarta.annotation.security.RolesAllowed({"**"}) on methods when:
      • Security is http (basic/bearer), apiKey, or oauth2/openIdConnect with empty scopes.
      • OR groups qualify without an anonymous ({}) alternative.
    • Respects global vs. per‑operation security.
    • Works alongside useMicroProfileOpenAPIAnnotations.
    • Added x-jakarta-any-roles vendor extension and a dedicated processor to keep template logic simple.
    • Extensive tests and Quarkus-focused fixtures to cover OR/AND edge cases.
  • Migration

    • Replace useQuarkusSecurityAnnotations with useJakartaSecurityAnnotations.
    • Requires useJakartaEe=true; only supported when library=quarkus.
    • Example: --additional-properties=useJakartaEe=true,useJakartaSecurityAnnotations=true,library=quarkus.

Written for commit 1b06135. Summary will update on new commits.

@Ignacio-Vidal Ignacio-Vidal changed the title [jaxrs-spec][quarkus] emit roles [jaxrs-spec][quarkus] Emit @RolesAllowed({scope}) for OAuth2 and OpenID Connect operations with explicit scopes May 10, 2026
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.

[jaxrs-spec][quarkus] - Emit Authentication & Authorisation annotations (@Authenticated, @RolesAllowed, @PermitAll) from Security Schemes

1 participant