diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AbstractAuthRequestBuilder.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AbstractAuthRequestBuilder.java index b145d9906..17f1d522a 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AbstractAuthRequestBuilder.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AbstractAuthRequestBuilder.java @@ -17,15 +17,24 @@ import static io.serverlessworkflow.api.types.OAuth2AuthenticationDataClient.ClientAuthentication.CLIENT_SECRET_POST; import static io.serverlessworkflow.impl.WorkflowUtils.isValid; +import static io.serverlessworkflow.impl.auth.AuthUtils.ACTOR; +import static io.serverlessworkflow.impl.auth.AuthUtils.ACTOR_TOKEN; +import static io.serverlessworkflow.impl.auth.AuthUtils.ACTOR_TOKEN_TYPE; import static io.serverlessworkflow.impl.auth.AuthUtils.AUDIENCES; import static io.serverlessworkflow.impl.auth.AuthUtils.AUTHENTICATION; import static io.serverlessworkflow.impl.auth.AuthUtils.CLIENT; import static io.serverlessworkflow.impl.auth.AuthUtils.ENCODING; import static io.serverlessworkflow.impl.auth.AuthUtils.REQUEST; import static io.serverlessworkflow.impl.auth.AuthUtils.SCOPES; +import static io.serverlessworkflow.impl.auth.AuthUtils.SUBJECT; +import static io.serverlessworkflow.impl.auth.AuthUtils.SUBJECT_TOKEN; +import static io.serverlessworkflow.impl.auth.AuthUtils.SUBJECT_TOKEN_TYPE; +import static io.serverlessworkflow.impl.auth.AuthUtils.TOKEN; +import static io.serverlessworkflow.impl.auth.AuthUtils.TYPE; import io.serverlessworkflow.api.types.OAuth2AuthenticationData; import io.serverlessworkflow.api.types.OAuth2AuthenticationDataClient; +import io.serverlessworkflow.api.types.OAuth2TokenDefinition; import io.serverlessworkflow.impl.WorkflowApplication; import io.serverlessworkflow.impl.WorkflowUtils; import java.util.Arrays; @@ -51,6 +60,7 @@ public HttpRequestInfo apply(T authenticationData) { audience(authenticationData); scope(authenticationData); authenticationMethod(authenticationData); + subjectActor(authenticationData); return requestBuilder.build(); } @@ -61,6 +71,7 @@ public HttpRequestInfo apply(Map secret) { audience(secret); scope(secret); authenticationMethod(secret); + subjectActor(secret); return requestBuilder.build(); } @@ -80,20 +91,17 @@ protected void audience(Map secret) { } protected void authenticationMethod(T authenticationData) { - ClientSecretHandler secretHandler; - switch (getClientAuthentication(authenticationData)) { - case CLIENT_SECRET_BASIC: - secretHandler = new ClientSecretBasic(application, requestBuilder); - case CLIENT_SECRET_JWT: - throw new UnsupportedOperationException("Client Secret JWT is not supported yet"); - case PRIVATE_KEY_JWT: - throw new UnsupportedOperationException("Private Key JWT is not supported yet"); - default: - secretHandler = new ClientSecretPost(application, requestBuilder); - } + ClientSecretHandler secretHandler = + switch (getClientAuthentication(authenticationData)) { + case CLIENT_SECRET_BASIC -> new ClientSecretBasic(application, requestBuilder); + case CLIENT_SECRET_JWT, PRIVATE_KEY_JWT -> + new JwtClientAssertion(application, requestBuilder); + default -> new ClientSecretPost(application, requestBuilder); + }; secretHandler.accept(authenticationData); } + @SuppressWarnings("unchecked") protected void authenticationMethod(Map secret) { Map client = (Map) secret.get(CLIENT); ClientSecretHandler secretHandler; @@ -101,23 +109,44 @@ protected void authenticationMethod(Map secret) { if (auth == null) { secretHandler = new ClientSecretPost(application, requestBuilder); } else { - switch (auth) { - case "client_secret_basic": - secretHandler = new ClientSecretBasic(application, requestBuilder); - break; - default: - case "client_secret_post": - secretHandler = new ClientSecretPost(application, requestBuilder); - break; - case "private_key_jwt": - throw new UnsupportedOperationException("Private Key JWT is not supported yet"); - case "client_secret_jwt": - throw new UnsupportedOperationException("Client Secret JWT is not supported yet"); - } + secretHandler = + switch (auth) { + case "client_secret_basic" -> new ClientSecretBasic(application, requestBuilder); + case "private_key_jwt", "client_secret_jwt" -> + new JwtClientAssertion(application, requestBuilder); + default -> new ClientSecretPost(application, requestBuilder); + }; } secretHandler.accept(secret); } + protected void subjectActor(T authenticationData) { + tokenParam(SUBJECT_TOKEN, SUBJECT_TOKEN_TYPE, authenticationData.getSubject()); + tokenParam(ACTOR_TOKEN, ACTOR_TOKEN_TYPE, authenticationData.getActor()); + } + + private void tokenParam(String tokenKey, String typeKey, OAuth2TokenDefinition definition) { + if (definition != null) { + requestBuilder + .addQueryParam( + tokenKey, WorkflowUtils.buildStringFilter(application, definition.getToken())) + .addQueryParam(typeKey, definition.getType()); + } + } + + protected void subjectActor(Map secret) { + tokenParam(SUBJECT_TOKEN, SUBJECT_TOKEN_TYPE, secret.get(SUBJECT)); + tokenParam(ACTOR_TOKEN, ACTOR_TOKEN_TYPE, secret.get(ACTOR)); + } + + private void tokenParam(String tokenKey, String typeKey, Object rawDefinition) { + if (rawDefinition instanceof Map definition) { + requestBuilder + .addQueryParam(tokenKey, (String) definition.get(TOKEN)) + .addQueryParam(typeKey, (String) definition.get(TYPE)); + } + } + private OAuth2AuthenticationDataClient.ClientAuthentication getClientAuthentication( OAuth2AuthenticationData authenticationData) { return authenticationData.getClient() == null diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthUtils.java index 7dc388827..28daba2a4 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthUtils.java @@ -35,6 +35,23 @@ private AuthUtils() {} public static final String REQUEST = "request"; public static final String ENCODING = "encoding"; public static final String AUTHENTICATION = "authentication"; + public static final String ASSERTION = "assertion"; + public static final String SUBJECT = "subject"; + public static final String ACTOR = "actor"; + public static final String TYPE = "type"; + public static final String REVOCATION = "revocation"; + public static final String INTROSPECTION = "introspection"; + + public static final String CLIENT_ID = "client_id"; + public static final String CLIENT_ASSERTION = "client_assertion"; + public static final String CLIENT_ASSERTION_TYPE = "client_assertion_type"; + public static final String JWT_BEARER_ASSERTION_TYPE = + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + + public static final String SUBJECT_TOKEN = "subject_token"; + public static final String SUBJECT_TOKEN_TYPE = "subject_token_type"; + public static final String ACTOR_TOKEN = "actor_token"; + public static final String ACTOR_TOKEN_TYPE = "actor_token_type"; private static final String AUTH_HEADER_FORMAT = "%s %s"; diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/ClientSecretHandler.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/ClientSecretHandler.java index b4be0f885..491f32ec9 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/ClientSecretHandler.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/ClientSecretHandler.java @@ -17,6 +17,7 @@ import static io.serverlessworkflow.api.types.OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.CLIENT_CREDENTIALS; import static io.serverlessworkflow.api.types.OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.PASSWORD; +import static io.serverlessworkflow.api.types.OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.URN_IETF_PARAMS_OAUTH_GRANT_TYPE_TOKEN_EXCHANGE; import io.serverlessworkflow.api.types.OAuth2AuthenticationData; import io.serverlessworkflow.impl.WorkflowApplication; @@ -48,7 +49,8 @@ void accept(OAuth2AuthenticationData authenticationData) { } password(authenticationData); - } else if (authenticationData.getGrant().equals(CLIENT_CREDENTIALS)) { + } else if (authenticationData.getGrant().equals(CLIENT_CREDENTIALS) + || authenticationData.getGrant().equals(URN_IETF_PARAMS_OAUTH_GRANT_TYPE_TOKEN_EXCHANGE)) { if (authenticationData.getClient() == null || authenticationData.getClient().getId() == null || authenticationData.getClient().getSecret() == null) { @@ -74,6 +76,7 @@ void accept(Map secret) { String grant = Objects.requireNonNull((String) secret.get("grant"), "Grant is mandatory field"); switch (grant) { case "client_credentials": + case "urn:ietf:params:oauth:grant-type:token-exchange": clientCredentials(secret); break; case "password": diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/HttpRequestInfo.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/HttpRequestInfo.java index c7a754079..b42c23972 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/HttpRequestInfo.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/HttpRequestInfo.java @@ -23,5 +23,7 @@ public record HttpRequestInfo( Map> headers, Map> queryParams, WorkflowValueResolver uri, + WorkflowValueResolver revocationUri, + WorkflowValueResolver introspectionUri, String grantType, String contentType) {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/HttpRequestInfoBuilder.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/HttpRequestInfoBuilder.java index c948ad3a1..71accf20a 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/HttpRequestInfoBuilder.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/HttpRequestInfoBuilder.java @@ -32,6 +32,10 @@ class HttpRequestInfoBuilder { private WorkflowValueResolver uri; + private WorkflowValueResolver revocationUri; + + private WorkflowValueResolver introspectionUri; + private String grantType; private String contentType; @@ -66,6 +70,16 @@ HttpRequestInfoBuilder withUri(WorkflowValueResolver uri) { return this; } + HttpRequestInfoBuilder withRevocationUri(WorkflowValueResolver revocationUri) { + this.revocationUri = revocationUri; + return this; + } + + HttpRequestInfoBuilder withIntrospectionUri(WorkflowValueResolver introspectionUri) { + this.introspectionUri = introspectionUri; + return this; + } + HttpRequestInfoBuilder withContentType(OAuth2TokenRequest oAuth2TokenRequest) { if (oAuth2TokenRequest != null) { this.contentType = oAuth2TokenRequest.getEncoding().value(); @@ -91,6 +105,7 @@ HttpRequestInfo build() { if (contentType == null) { contentType = APPLICATION_X_WWW_FORM_URLENCODED.value(); } - return new HttpRequestInfo(headers, queryParams, uri, grantType, contentType); + return new HttpRequestInfo( + headers, queryParams, uri, revocationUri, introspectionUri, grantType, contentType); } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/JwtClientAssertion.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/JwtClientAssertion.java new file mode 100644 index 000000000..2a26e9326 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/JwtClientAssertion.java @@ -0,0 +1,130 @@ +/* + * 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.impl.auth; + +import static io.serverlessworkflow.api.types.OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.PASSWORD; +import static io.serverlessworkflow.impl.auth.AuthUtils.ASSERTION; +import static io.serverlessworkflow.impl.auth.AuthUtils.CLIENT; +import static io.serverlessworkflow.impl.auth.AuthUtils.CLIENT_ASSERTION; +import static io.serverlessworkflow.impl.auth.AuthUtils.CLIENT_ASSERTION_TYPE; +import static io.serverlessworkflow.impl.auth.AuthUtils.CLIENT_ID; +import static io.serverlessworkflow.impl.auth.AuthUtils.GRANT; +import static io.serverlessworkflow.impl.auth.AuthUtils.ID; +import static io.serverlessworkflow.impl.auth.AuthUtils.JWT_BEARER_ASSERTION_TYPE; +import static io.serverlessworkflow.impl.auth.AuthUtils.USER; + +import io.serverlessworkflow.api.types.OAuth2AuthenticationData; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowUtils; +import java.util.Map; + +/** + * Handles the {@code client_secret_jwt} and {@code private_key_jwt} client authentication methods. + * + *

Per the Serverless Workflow specification, the caller supplies a pre-signed JWT through {@code + * client.assertion}. Both methods are forwarded identically: the assertion is sent as {@code + * client_assertion} together with the standard {@code client_assertion_type} defined by RFC 7523. + * The signing algorithm (HMAC for {@code client_secret_jwt}, an asymmetric key for {@code + * private_key_jwt}) is the caller's responsibility. + */ +class JwtClientAssertion extends ClientSecretHandler { + + protected JwtClientAssertion( + WorkflowApplication application, HttpRequestInfoBuilder requestBuilder) { + super(application, requestBuilder); + } + + @Override + void accept(OAuth2AuthenticationData authenticationData) { + if (authenticationData.getClient() == null + || authenticationData.getClient().getAssertion() == null) { + throw new IllegalArgumentException( + "A client assertion must be provided for JWT client authentication"); + } + if (authenticationData.getGrant().equals(PASSWORD)) { + if (authenticationData.getUsername() == null || authenticationData.getPassword() == null) { + throw new IllegalArgumentException( + "Username and password must be provided for password grant type"); + } + password(authenticationData); + } else { + clientCredentials(authenticationData); + } + } + + @Override + void accept(Map secret) { + Map client = asClient(secret); + if (client == null || client.get(ASSERTION) == null) { + throw new IllegalArgumentException( + "A client assertion must be provided for JWT client authentication"); + } + if (PASSWORD.value().equals(secret.get(GRANT))) { + password(secret); + } else { + clientCredentials(secret); + } + } + + @Override + protected void clientCredentials(OAuth2AuthenticationData authenticationData) { + requestBuilder.withGrantType(authenticationData.getGrant().value()); + addAssertion( + authenticationData.getClient().getId(), authenticationData.getClient().getAssertion()); + } + + @Override + protected void password(OAuth2AuthenticationData authenticationData) { + clientCredentials(authenticationData); + requestBuilder + .addQueryParam( + "username", + WorkflowUtils.buildStringFilter(application, authenticationData.getUsername())) + .addQueryParam( + "password", + WorkflowUtils.buildStringFilter(application, authenticationData.getPassword())); + } + + @Override + protected void clientCredentials(Map secret) { + Map client = asClient(secret); + requestBuilder.withGrantType((String) secret.get(GRANT)); + addAssertion((String) client.get(ID), (String) client.get(ASSERTION)); + } + + @Override + protected void password(Map secret) { + clientCredentials(secret); + requestBuilder + .addQueryParam("username", (String) secret.get(USER)) + .addQueryParam("password", (String) secret.get(AuthUtils.PASSWORD)); + } + + private void addAssertion(String clientId, String assertion) { + if (clientId != null) { + requestBuilder.addQueryParam( + CLIENT_ID, WorkflowUtils.buildStringFilter(application, clientId)); + } + requestBuilder + .addQueryParam(CLIENT_ASSERTION_TYPE, JWT_BEARER_ASSERTION_TYPE) + .addQueryParam(CLIENT_ASSERTION, WorkflowUtils.buildStringFilter(application, assertion)); + } + + @SuppressWarnings("unchecked") + private static Map asClient(Map secret) { + return (Map) secret.get(CLIENT); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/OAuthRequestBuilder.java b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/OAuthRequestBuilder.java index a4766e335..9f39227e0 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/auth/OAuthRequestBuilder.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/auth/OAuthRequestBuilder.java @@ -30,35 +30,54 @@ class OAuthRequestBuilder extends AbstractAuthRequestBuilder { private static String DEFAULT_TOKEN_PATH = "oauth2/token"; + private static String DEFAULT_REVOCATION_PATH = "oauth2/revoke"; + private static String DEFAULT_INTROSPECTION_PATH = "oauth2/introspect"; public OAuthRequestBuilder(WorkflowApplication application) { super(application); } - // TODO handle revocation and introspection path - // private static String DEFAULT_REVOCATION_PATH = "oauth2/revoke"; - // private static String DEFAULT_INTROSPECTION_PATH = "oauth2/introspect"; - @Override protected void authenticationURI(OAuth2ConnectAuthenticationProperties authenticationData) { OAuth2AuthenticationPropertiesEndpoints endpoints = authenticationData.getEndpoints(); WorkflowValueResolver uri = WorkflowUtils.getURISupplier(application, authenticationData.getAuthority()); - String tokenPath = - endpoints != null && endpoints.getToken() != null - ? endpoints.getToken().replaceAll("^/", "") - : DEFAULT_TOKEN_PATH; - requestBuilder.withUri((w, t, m) -> concatURI(uri.apply(w, t, m), tokenPath)); + String token = endpoints != null ? endpoints.getToken() : null; + String revocation = endpoints != null ? endpoints.getRevocation() : null; + String introspection = endpoints != null ? endpoints.getIntrospection() : null; + requestBuilder + .withUri(endpointResolver(uri, endpointPath(token, DEFAULT_TOKEN_PATH))) + .withRevocationUri(endpointResolver(uri, endpointPath(revocation, DEFAULT_REVOCATION_PATH))) + .withIntrospectionUri( + endpointResolver(uri, endpointPath(introspection, DEFAULT_INTROSPECTION_PATH))); } @Override protected void authenticationURI(Map secret) { - String tokenPath = - secret.get("endpoints") instanceof Map endpoints ? (String) endpoints.get("token") : null; - URI uri = - concatURI( - URI.create((String) secret.get(AUTHORITY)), - tokenPath == null ? DEFAULT_TOKEN_PATH : tokenPath); - requestBuilder.withUri((w, t, m) -> uri); + URI authority = URI.create((String) secret.get(AUTHORITY)); + Map endpoints = + secret.get("endpoints") instanceof Map raw ? (Map) raw : Map.of(); + requestBuilder + .withUri(staticUri(authority, endpoints, "token", DEFAULT_TOKEN_PATH)) + .withRevocationUri(staticUri(authority, endpoints, "revocation", DEFAULT_REVOCATION_PATH)) + .withIntrospectionUri( + staticUri(authority, endpoints, "introspection", DEFAULT_INTROSPECTION_PATH)); + } + + private static String endpointPath(String path, String defaultPath) { + return path != null ? path.replaceAll("^/", "") : defaultPath; + } + + private WorkflowValueResolver endpointResolver( + WorkflowValueResolver authority, String path) { + return (w, t, m) -> concatURI(authority.apply(w, t, m), path); + } + + private static WorkflowValueResolver staticUri( + URI authority, Map endpoints, String key, String defaultPath) { + String path = + endpoints.get(key) instanceof String value ? endpointPath(value, defaultPath) : defaultPath; + URI uri = concatURI(authority, path); + return (w, t, m) -> uri; } } diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/OAuthHTTPWorkflowDefinitionTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/OAuthHTTPWorkflowDefinitionTest.java index 541d04c10..acad771c3 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/OAuthHTTPWorkflowDefinitionTest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/OAuthHTTPWorkflowDefinitionTest.java @@ -18,12 +18,15 @@ import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; import static io.serverlessworkflow.impl.test.AccessTokenProvider.fakeAccessToken; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.ObjectMapper; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.impl.WorkflowApplication; import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.Map; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -820,4 +823,90 @@ public void testOAuthJSONClientCredentialsParamsNoEndpointWorkflowExecution() th assertEquals("/hello", petRequest.getPath()); assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization")); } + + @Test + public void testOAuthClientSecretJwtClientCredentialsWorkflowExecution() throws Exception { + String assertion = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.client-secret-jwt-assertion.signature"; + String tokenRequestBody = + runJwtClientAuthWorkflow( + "workflows-samples/oauth2/oAuthClientSecretJwtClientCredentialsHttpCall.yaml"); + + assertTrue(tokenRequestBody.contains("grant_type=client_credentials")); + assertTrue(tokenRequestBody.contains("client_id=serverless-workflow")); + assertTrue( + tokenRequestBody.contains( + "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer")); + assertTrue(tokenRequestBody.contains("client_assertion=" + assertion)); + assertFalse(tokenRequestBody.contains("client_secret=")); + } + + @Test + public void testOAuthPrivateKeyJwtClientCredentialsWorkflowExecution() throws Exception { + String assertion = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.private-key-jwt-assertion.signature"; + String tokenRequestBody = + runJwtClientAuthWorkflow( + "workflows-samples/oauth2/oAuthPrivateKeyJwtClientCredentialsHttpCall.yaml"); + + assertTrue(tokenRequestBody.contains("grant_type=client_credentials")); + assertTrue(tokenRequestBody.contains("client_id=serverless-workflow")); + assertTrue( + tokenRequestBody.contains( + "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer")); + assertTrue(tokenRequestBody.contains("client_assertion=" + assertion)); + assertFalse(tokenRequestBody.contains("client_secret=")); + } + + @Test + public void testOAuthTokenExchangeSubjectActorWorkflowExecution() throws Exception { + String tokenRequestBody = + runJwtClientAuthWorkflow( + "workflows-samples/oauth2/oAuthClientSecretPostTokenExchangeHttpCall.yaml"); + + assertTrue( + tokenRequestBody.contains("grant_type=urn:ietf:params:oauth:grant-type:token-exchange")); + assertTrue(tokenRequestBody.contains("subject_token=subject-token-value")); + assertTrue( + tokenRequestBody.contains( + "subject_token_type=urn:ietf:params:oauth:token-type:access_token")); + assertTrue(tokenRequestBody.contains("actor_token=actor-token-value")); + assertTrue( + tokenRequestBody.contains( + "actor_token_type=urn:ietf:params:oauth:token-type:access_token")); + } + + private String runJwtClientAuthWorkflow(String workflowResource) throws Exception { + String jwt = fakeAccessToken(); + String tokenResponse = TOKEN_RESPONSE_TEMPLATE.formatted(jwt); + + authServer.enqueue( + new MockResponse() + .setBody(tokenResponse) + .setHeader("Content-Type", "application/json") + .setResponseCode(200)); + + apiServer.enqueue( + new MockResponse() + .setBody(RESPONSE) + .setHeader("Content-Type", "application/json") + .setResponseCode(200)); + + Workflow workflow = readWorkflowFromClasspath(workflowResource); + Map result = + app.workflowDefinition(workflow).instance(Map.of()).start().get().asMap().orElseThrow(); + + assertTrue(result.containsKey("message")); + assertTrue(result.get("message").toString().contains("Hello World")); + + RecordedRequest tokenRequest = authServer.takeRequest(); + assertEquals("POST", tokenRequest.getMethod()); + assertEquals("/realms/test-realm/protocol/openid-connect/token", tokenRequest.getPath()); + assertEquals("application/x-www-form-urlencoded", tokenRequest.getHeader("Content-Type")); + + RecordedRequest petRequest = apiServer.takeRequest(); + assertEquals("GET", petRequest.getMethod()); + assertEquals("/hello", petRequest.getPath()); + assertEquals("Bearer " + jwt, petRequest.getHeader("Authorization")); + + return URLDecoder.decode(tokenRequest.getBody().readUtf8(), StandardCharsets.UTF_8); + } } diff --git a/impl/test/src/test/resources/workflows-samples/oauth2/oAuthClientSecretJwtClientCredentialsHttpCall.yaml b/impl/test/src/test/resources/workflows-samples/oauth2/oAuthClientSecretJwtClientCredentialsHttpCall.yaml new file mode 100644 index 000000000..a2ddb9c8d --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/oauth2/oAuthClientSecretJwtClientCredentialsHttpCall.yaml @@ -0,0 +1,24 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: oauth2-authentication-client-secret-jwt-client-credentials + version: '0.0.1' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: http://localhost:8081/hello + authentication: + oauth2: + authority: http://localhost:8888/realms/test-realm + endpoints: + token: protocol/openid-connect/token + grant: client_credentials + client: + id: serverless-workflow + authentication: client_secret_jwt + assertion: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.client-secret-jwt-assertion.signature + issuers: + - http://localhost:8888/realms/test-realm diff --git a/impl/test/src/test/resources/workflows-samples/oauth2/oAuthClientSecretPostTokenExchangeHttpCall.yaml b/impl/test/src/test/resources/workflows-samples/oauth2/oAuthClientSecretPostTokenExchangeHttpCall.yaml new file mode 100644 index 000000000..1beeee1c6 --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/oauth2/oAuthClientSecretPostTokenExchangeHttpCall.yaml @@ -0,0 +1,29 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: oauth2-authentication-token-exchange + version: '0.0.1' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: http://localhost:8081/hello + authentication: + oauth2: + authority: http://localhost:8888/realms/test-realm + endpoints: + token: protocol/openid-connect/token + grant: urn:ietf:params:oauth:grant-type:token-exchange + client: + id: serverless-workflow + secret: D0ACXCUKOUrL5YL7j6RQWplMaSjPB8MT + subject: + token: subject-token-value + type: urn:ietf:params:oauth:token-type:access_token + actor: + token: actor-token-value + type: urn:ietf:params:oauth:token-type:access_token + issuers: + - http://localhost:8888/realms/test-realm diff --git a/impl/test/src/test/resources/workflows-samples/oauth2/oAuthPrivateKeyJwtClientCredentialsHttpCall.yaml b/impl/test/src/test/resources/workflows-samples/oauth2/oAuthPrivateKeyJwtClientCredentialsHttpCall.yaml new file mode 100644 index 000000000..dc69c6df0 --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/oauth2/oAuthPrivateKeyJwtClientCredentialsHttpCall.yaml @@ -0,0 +1,24 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: oauth2-authentication-private-key-jwt-client-credentials + version: '0.0.1' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: http://localhost:8081/hello + authentication: + oauth2: + authority: http://localhost:8888/realms/test-realm + endpoints: + token: protocol/openid-connect/token + grant: client_credentials + client: + id: serverless-workflow + authentication: private_key_jwt + assertion: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.private-key-jwt-assertion.signature + issuers: + - http://localhost:8888/realms/test-realm