diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java index f06d8d90c..18f75dd08 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java @@ -39,6 +39,8 @@ import io.serverlessworkflow.fluent.func.configurers.SwitchCaseConfigurer; import io.serverlessworkflow.fluent.spec.AbstractEventConsumptionStrategyBuilder; import io.serverlessworkflow.fluent.spec.EventFilterBuilder; +import io.serverlessworkflow.fluent.spec.OAuth2AuthenticationPolicyBuilder; +import io.serverlessworkflow.fluent.spec.OAuth2AuthenticationPolicyBuilder.OAuth2AuthenticationPropertiesEndpointsBuilder; import io.serverlessworkflow.fluent.spec.ScheduleBuilder; import io.serverlessworkflow.fluent.spec.TimeoutBuilder; import io.serverlessworkflow.fluent.spec.WorkflowTaskBuilder; @@ -2371,4 +2373,24 @@ public static AuthenticationConfigurer oauth2( public static AuthenticationConfigurer oauth2(String secret) { return DSL.oauth2(secret); } + + /** + * @see DSL#oauth2(String, OAuth2AuthenticationData.OAuth2AuthenticationDataGrant, String, String, + * Consumer) + */ + public static AuthenticationConfigurer oauth2( + String authority, + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant grant, + String clientId, + String clientSecret, + Consumer endpoints) { + return DSL.oauth2(authority, grant, clientId, clientSecret, endpoints); + } + + /** + * @see DSL#oauth2(Consumer) + */ + public static AuthenticationConfigurer oauth2(Consumer cfg) { + return DSL.oauth2(cfg); + } } diff --git a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLOAuth2Test.java b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLOAuth2Test.java new file mode 100644 index 000000000..76a382c3b --- /dev/null +++ b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLOAuth2Test.java @@ -0,0 +1,131 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.func; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.call; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.http; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.oauth2; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.serverlessworkflow.api.types.OAuth2AuthenticationData; +import io.serverlessworkflow.api.types.OAuth2AuthenticationDataClient; +import io.serverlessworkflow.api.types.OAuth2ConnectAuthenticationProperties; +import io.serverlessworkflow.api.types.Workflow; +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.Test; + +class FuncDSLOAuth2Test { + + private static final String EXPR_ENDPOINT = "${ .endpoint }"; + + private static OAuth2ConnectAuthenticationProperties oauth2PropertiesOf(Workflow wf) { + var auth = + wf.getDo() + .get(0) + .getTask() + .getCallTask() + .getCallHTTP() + .getWith() + .getEndpoint() + .getEndpointConfiguration() + .getAuthentication() + .getAuthenticationPolicy(); + assertNotNull(auth.getOAuth2AuthenticationPolicy()); + return auth.getOAuth2AuthenticationPolicy() + .getOauth2() + .getOAuth2ConnectAuthenticationProperties(); + } + + @Test + void convenience_overload_sets_token_endpoint() { + Workflow wf = + FuncWorkflowBuilder.workflow("oauth2-token") + .tasks( + call( + http() + .POST() + .endpoint( + EXPR_ENDPOINT, + oauth2( + "https://auth.example.com/", + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant + .CLIENT_CREDENTIALS, + "client-id", + "client-secret", + e -> e.token("/custom/token"))))) + .build(); + + var props = oauth2PropertiesOf(wf); + assertEquals(URI.create("https://auth.example.com/"), props.getAuthority().getLiteralUri()); + assertEquals( + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.CLIENT_CREDENTIALS, + props.getGrant()); + assertEquals("client-id", props.getClient().getId()); + assertEquals("client-secret", props.getClient().getSecret()); + assertEquals("/custom/token", props.getEndpoints().getToken()); + } + + @Test + void builder_overload_supports_full_oauth2_section() { + Workflow wf = + FuncWorkflowBuilder.workflow("oauth2-full") + .tasks( + call( + http() + .GET() + .endpoint( + EXPR_ENDPOINT, + oauth2( + o -> + o.endpoints( + e -> + e.token("/oauth2/token") + .revocation("/oauth2/revoke") + .introspection("/oauth2/introspect")) + .authority("https://auth.example.com/") + .grant( + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant + .CLIENT_CREDENTIALS) + .scopes("read", "write") + .audiences("api://default") + .client( + c -> + c.id("client-id") + .secret("client-secret") + .authentication( + OAuth2AuthenticationDataClient + .ClientAuthentication + .CLIENT_SECRET_BASIC)))))) + .build(); + + var props = oauth2PropertiesOf(wf); + assertEquals(URI.create("https://auth.example.com/"), props.getAuthority().getLiteralUri()); + assertEquals( + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.CLIENT_CREDENTIALS, + props.getGrant()); + assertEquals(List.of("read", "write"), props.getScopes()); + assertEquals(List.of("api://default"), props.getAudiences()); + assertEquals("client-id", props.getClient().getId()); + assertEquals( + OAuth2AuthenticationDataClient.ClientAuthentication.CLIENT_SECRET_BASIC, + props.getClient().getAuthentication()); + assertEquals("/oauth2/token", props.getEndpoints().getToken()); + assertEquals("/oauth2/revoke", props.getEndpoints().getRevocation()); + assertEquals("/oauth2/introspect", props.getEndpoints().getIntrospection()); + } +} diff --git a/experimental/test/pom.xml b/experimental/test/pom.xml index 6cf53a5cd..85f3d96c1 100644 --- a/experimental/test/pom.xml +++ b/experimental/test/pom.xml @@ -79,6 +79,12 @@ ${project.version} test + + io.serverlessworkflow + serverlessworkflow-impl-jackson-jwt + ${project.version} + test + org.glassfish.jersey.media jersey-media-json-jackson diff --git a/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncOAuth2HttpTest.java b/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncOAuth2HttpTest.java new file mode 100644 index 000000000..e6c06be75 --- /dev/null +++ b/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncOAuth2HttpTest.java @@ -0,0 +1,261 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.serverlessworkflow.api.types.OAuth2AuthenticationData; +import io.serverlessworkflow.api.types.OAuth2AuthenticationDataClient; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; +import io.serverlessworkflow.fluent.func.dsl.FuncDSL; +import io.serverlessworkflow.impl.WorkflowApplication; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import mockwebserver3.Dispatcher; +import mockwebserver3.MockResponse; +import mockwebserver3.MockWebServer; +import mockwebserver3.RecordedRequest; +import okhttp3.Headers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class FuncOAuth2HttpTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private record OAuth2Client(String clientId, String clientSecret, String baseUrl) {} + + private WorkflowApplication app; + private MockWebServer mockServer; + + // Recorded token-request bodies, keyed by the request path. + private final Map tokenRequestBodies = new ConcurrentHashMap<>(); + // Recorded Authorization headers of the downstream API calls, keyed by the request path. + private final Map apiAuthHeaders = new ConcurrentHashMap<>(); + + @BeforeEach + void setup() throws IOException { + app = WorkflowApplication.builder().build(); + mockServer = new MockWebServer(); + mockServer.setDispatcher( + new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + String path = request.getUrl().encodedPath(); + if (path.endsWith("/token")) { + tokenRequestBodies.put(path, request.getBody().utf8()); + return new MockResponse( + 200, Headers.of("Content-Type", "application/json"), tokenResponse(fakeJwt())); + } + // The downstream API call ("/joogle" or "/jahoo"). + apiAuthHeaders.put(path, request.getHeaders().get("Authorization")); + return new MockResponse( + 200, + Headers.of("Content-Type", "application/json"), + "{\"email\":\"" + path + "@example.com\"}"); + } + }); + mockServer.start(0); + } + + @AfterEach + void cleanup() { + mockServer.close(); + app.close(); + } + + @Test + @DisplayName( + "Two named OAuth2 client-credentials authentications, each used by a forked HTTP call") + void test_multiple_oauth2_clients() throws Exception { + String base = mockServer.url("/").toString(); + base = base.substring(0, base.length() - 1); // strip trailing slash + + OAuth2Client joogle = + new OAuth2Client("joogle-client-id", "joogle-client-secret", base + "/joogle-auth"); + OAuth2Client jahoo = + new OAuth2Client("jahoo-client-id", "jahoo-client-secret", base + "/jahoo-auth"); + String wireMock = base; + + Workflow workflow = + FuncWorkflowBuilder.workflow("multiple-oauth2-clients", "quarkus-flow") + .use( + use -> + use.authentications( + auth -> { + auth.authentication( + "joogle", + a -> + a.oauth2( + oauth2 -> + oauth2 + .client( + client -> + client + .id(joogle.clientId()) + .secret(joogle.clientSecret()) + .authentication( + OAuth2AuthenticationDataClient + .ClientAuthentication + .CLIENT_SECRET_POST)) + .authority(joogle.baseUrl()) + .grant( + OAuth2AuthenticationData + .OAuth2AuthenticationDataGrant + .CLIENT_CREDENTIALS) + .build())); + auth.authentication( + "jahoo", + a -> + a.oauth2( + oauth2 -> + oauth2 + .client( + client -> + client + .id(jahoo.clientId()) + .secret(jahoo.clientSecret())) + .authority(jahoo.baseUrl()) + .grant( + OAuth2AuthenticationData + .OAuth2AuthenticationDataGrant + .CLIENT_CREDENTIALS) + .build())); + })) + .tasks( + FuncDSL.fork( + FuncDSL.http() + .GET() + .uri(URI.create(wireMock + "/joogle"), FuncDSL.use("joogle")), + FuncDSL.http() + .GET() + .uri(URI.create(wireMock + "/jahoo"), FuncDSL.use("jahoo"))), + FuncDSL.function("merge", o -> o)) + .build(); + + app.workflowDefinition(workflow).instance(Map.of()).start().join(); + + String joogleTokenBody = tokenRequestBodies.get("/joogle-auth/oauth2/token"); + String jahooTokenBody = tokenRequestBodies.get("/jahoo-auth/oauth2/token"); + + assertThat(joogleTokenBody) + .as("joogle token request body") + .isNotNull() + .contains("grant_type=client_credentials") + .contains("client_id=joogle-client-id") + .contains("client_secret=joogle-client-secret"); + + assertThat(jahooTokenBody) + .as("jahoo token request body") + .isNotNull() + .contains("grant_type=client_credentials") + .contains("client_id=jahoo-client-id") + .contains("client_secret=jahoo-client-secret"); + + // The token obtained from each authority is forwarded as a Bearer token on the API call. + assertThat(apiAuthHeaders.get("/joogle")).as("joogle bearer").startsWith("Bearer "); + assertThat(apiAuthHeaders.get("/jahoo")).as("jahoo bearer").startsWith("Bearer "); + } + + @Test + @DisplayName("Custom endpoints.token overrides the default /oauth2/token path") + void test_custom_token_endpoint() throws Exception { + String base = mockServer.url("/").toString(); + base = base.substring(0, base.length() - 1); // strip trailing slash + + OAuth2Client joogle = + new OAuth2Client("joogle-client-id", "joogle-client-secret", base + "/joogle-auth"); + String wireMock = base; + String customTokenPath = "/auth/realms/joogle/protocol/openid-connect/token"; + + // Same configuration as above, expressed with the FuncDSL auth helpers: + // FuncDSL.auth(name, configurer) returns a chainable UseSpec (a Consumer), + // and FuncDSL.oauth2(...) builds the policy (no explicit .build() needed). + Workflow workflow = + FuncWorkflowBuilder.workflow("custom-token-endpoint", "quarkus-flow") + .use( + FuncDSL.auth( + "joogle", + FuncDSL.oauth2( + oauth2 -> + oauth2 + .endpoints(e -> e.token(customTokenPath)) + .client( + client -> + client + .id(joogle.clientId()) + .secret(joogle.clientSecret()) + .authentication( + OAuth2AuthenticationDataClient.ClientAuthentication + .CLIENT_SECRET_POST)) + .authority(joogle.baseUrl()) + .grant( + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant + .CLIENT_CREDENTIALS)))) + .tasks( + FuncDSL.http().GET().uri(URI.create(wireMock + "/joogle"), FuncDSL.use("joogle"))) + .build(); + + app.workflowDefinition(workflow).instance(Map.of()).start().join(); + + // The token path is resolved relative to the authority, so the default "/oauth2/token" is + // replaced by the custom path. + assertThat(tokenRequestBodies.get("/joogle-auth" + customTokenPath)) + .as("token request hit the custom endpoint") + .isNotNull() + .contains("client_id=joogle-client-id"); + assertThat(tokenRequestBodies).doesNotContainKey("/joogle-auth/oauth2/token"); + } + + private static String tokenResponse(String jwt) { + return """ + { + "access_token": "%s", + "token_type": "Bearer", + "expires_in": 3600 + } + """ + .formatted(jwt); + } + + private static String fakeJwt() { + try { + long now = Instant.now().getEpochSecond(); + String header = + MAPPER.writeValueAsString(Map.of("alg", "RS256", "typ", "Bearer", "kid", "test")); + String payload = + MAPPER.writeValueAsString(Map.of("sub", "test-subject", "exp", now + 3600, "iat", now)); + return b64Url(header) + "." + b64Url(payload) + ".sig"; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String b64Url(String s) { + return Base64.getUrlEncoder() + .withoutPadding() + .encodeToString(s.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/OAuth2AuthenticationPolicyBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/OAuth2AuthenticationPolicyBuilder.java index 1b64e2d72..b32cfba6e 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/OAuth2AuthenticationPolicyBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/OAuth2AuthenticationPolicyBuilder.java @@ -17,6 +17,7 @@ import io.serverlessworkflow.api.types.OAuth2AuthenticationPolicy; import io.serverlessworkflow.api.types.OAuth2AuthenticationPolicyConfiguration; +import io.serverlessworkflow.api.types.OAuth2AuthenticationPropertiesEndpoints; import java.util.function.Consumer; public final class OAuth2AuthenticationPolicyBuilder @@ -49,4 +50,31 @@ public OAuth2AuthenticationPolicy build() { policy.setOauth2(configuration); return policy; } + + public static final class OAuth2AuthenticationPropertiesEndpointsBuilder { + private final OAuth2AuthenticationPropertiesEndpoints endpoints; + + OAuth2AuthenticationPropertiesEndpointsBuilder() { + endpoints = new OAuth2AuthenticationPropertiesEndpoints(); + } + + public OAuth2AuthenticationPropertiesEndpointsBuilder token(String token) { + this.endpoints.setToken(token); + return this; + } + + public OAuth2AuthenticationPropertiesEndpointsBuilder revocation(String revocation) { + this.endpoints.setRevocation(revocation); + return this; + } + + public OAuth2AuthenticationPropertiesEndpointsBuilder introspection(String introspection) { + this.endpoints.setIntrospection(introspection); + return this; + } + + public OAuth2AuthenticationPropertiesEndpoints build() { + return this.endpoints; + } + } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/OIDCBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/OIDCBuilder.java index e2a3ac153..f4bd17fde 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/OIDCBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/OIDCBuilder.java @@ -18,7 +18,6 @@ import io.serverlessworkflow.api.types.AuthenticationPolicy; import io.serverlessworkflow.api.types.OAuth2AuthenticationData; import io.serverlessworkflow.api.types.OAuth2AuthenticationDataClient; -import io.serverlessworkflow.api.types.OAuth2AuthenticationPropertiesEndpoints; import io.serverlessworkflow.api.types.OAuth2ConnectAuthenticationProperties; import io.serverlessworkflow.api.types.OAuth2TokenDefinition; import io.serverlessworkflow.api.types.OAuth2TokenRequest; @@ -168,31 +167,4 @@ public OAuth2AuthenticationDataClient build() { return this.client; } } - - public static final class OAuth2AuthenticationPropertiesEndpointsBuilder { - private final OAuth2AuthenticationPropertiesEndpoints endpoints; - - OAuth2AuthenticationPropertiesEndpointsBuilder() { - endpoints = new OAuth2AuthenticationPropertiesEndpoints(); - } - - public OAuth2AuthenticationPropertiesEndpointsBuilder token(String token) { - this.endpoints.setToken(token); - return this; - } - - public OAuth2AuthenticationPropertiesEndpointsBuilder revocation(String revocation) { - this.endpoints.setRevocation(revocation); - return this; - } - - public OAuth2AuthenticationPropertiesEndpointsBuilder introspection(String introspection) { - this.endpoints.setIntrospection(introspection); - return this; - } - - public OAuth2AuthenticationPropertiesEndpoints build() { - return this.endpoints; - } - } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java index 2dc7323e2..f62e2f2cb 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java @@ -21,6 +21,8 @@ import io.serverlessworkflow.fluent.spec.EmitTaskBuilder; import io.serverlessworkflow.fluent.spec.EventFilterBuilder; import io.serverlessworkflow.fluent.spec.ForkTaskBuilder; +import io.serverlessworkflow.fluent.spec.OAuth2AuthenticationPolicyBuilder; +import io.serverlessworkflow.fluent.spec.OAuth2AuthenticationPolicyBuilder.OAuth2AuthenticationPropertiesEndpointsBuilder; import io.serverlessworkflow.fluent.spec.ScheduleBuilder; import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; import io.serverlessworkflow.fluent.spec.TimeoutBuilder; @@ -480,6 +482,75 @@ public static AuthenticationConfigurer oauth2(String secret) { return a -> a.openIDConnect(o -> o.use(secret)); } + /** + * Build an OAuth2 authentication configurer with client credentials and explicit OAuth2 {@code + * endpoints} (token, revocation, introspection). + * + *

Unlike the other {@code oauth2(...)} helpers, which delegate to OpenID Connect, this + * overload produces a genuine OAuth2 authentication policy so that the OAuth2-specific {@code + * endpoints} can be configured without hand-building the whole policy. + * + *

{@code
+   * oauth2(
+   *     "https://auth.example.com/",
+   *     OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.CLIENT_CREDENTIALS,
+   *     "client-id",
+   *     "client-secret",
+   *     e -> e.token("/custom/token"));
+   * }
+ * + * @param authority OAuth2 authority URL (the authorization server base URL) + * @param grant OAuth2 grant type + * @param clientId client identifier + * @param clientSecret client secret + * @param endpoints consumer that configures the OAuth2 endpoints (e.g. {@code e -> e.token(...)}) + * @return an {@link AuthenticationConfigurer} configured as OAuth2 with custom endpoints + */ + public static AuthenticationConfigurer oauth2( + String authority, + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant grant, + String clientId, + String clientSecret, + Consumer endpoints) { + return a -> + a.oauth2( + o -> { + o.authority(authority).grant(grant).client(c -> c.id(clientId).secret(clientSecret)); + o.endpoints(endpoints); + }); + } + + /** + * Build a fully customizable OAuth2 authentication configurer. + * + *

This overload exposes the complete {@link OAuth2AuthenticationPolicyBuilder} so that every + * field of the OAuth2 + * authentication section of the Serverless Workflow DSL can be configured: {@code authority}, + * {@code grant}, {@code client}, {@code request} encoding, {@code issuers}, {@code scopes}, + * {@code audiences}, {@code username}, {@code password}, {@code subject}, {@code actor} and + * {@code endpoints} (token, revocation, introspection). + * + *

Because the OAuth2-only {@code endpoints(...)} method is declared on {@link + * OAuth2AuthenticationPolicyBuilder} (not on the shared {@link + * io.serverlessworkflow.fluent.spec.OIDCBuilder}), call it first when chaining fluently: + * + *

{@code
+   * oauth2(o -> o.endpoints(e -> e.token("/custom/token"))
+   *              .authority("https://auth.example.com/")
+   *              .grant(OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.CLIENT_CREDENTIALS)
+   *              .scopes("read", "write")
+   *              .audiences("api://default")
+   *              .client(c -> c.id("client-id").secret("client-secret")));
+   * }
+ * + * @param cfg consumer that configures the OAuth2 authentication policy builder + * @return an {@link AuthenticationConfigurer} configured as OAuth2 + */ + public static AuthenticationConfigurer oauth2(Consumer cfg) { + return a -> a.oauth2(cfg); + } + /** * Build a {@link RaiseSpec} for an error with a string type expression and HTTP status. *