Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -51,6 +60,7 @@ public HttpRequestInfo apply(T authenticationData) {
audience(authenticationData);
scope(authenticationData);
authenticationMethod(authenticationData);
subjectActor(authenticationData);
return requestBuilder.build();
}

Expand All @@ -61,6 +71,7 @@ public HttpRequestInfo apply(Map<String, Object> secret) {
audience(secret);
scope(secret);
authenticationMethod(secret);
subjectActor(secret);
return requestBuilder.build();
}

Expand All @@ -80,44 +91,62 @@ protected void audience(Map<String, Object> 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<String, Object> secret) {
Map<String, Object> client = (Map<String, Object>) secret.get(CLIENT);
ClientSecretHandler secretHandler;
String auth = (String) client.get(AUTHENTICATION);
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<String, Object> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -74,6 +76,7 @@ void accept(Map<String, Object> 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":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ public record HttpRequestInfo(
Map<String, WorkflowValueResolver<String>> headers,
Map<String, WorkflowValueResolver<String>> queryParams,
WorkflowValueResolver<URI> uri,
WorkflowValueResolver<URI> revocationUri,
WorkflowValueResolver<URI> introspectionUri,
Comment on lines +26 to +27

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think these ones should be optional

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I see those fields are added, but where are we using them?

String grantType,
String contentType) {}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class HttpRequestInfoBuilder {

private WorkflowValueResolver<URI> uri;

private WorkflowValueResolver<URI> revocationUri;

private WorkflowValueResolver<URI> introspectionUri;
Comment on lines +35 to +37

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think these two ones should be Optional


private String grantType;

private String contentType;
Expand Down Expand Up @@ -66,6 +70,16 @@ HttpRequestInfoBuilder withUri(WorkflowValueResolver<URI> uri) {
return this;
}

HttpRequestInfoBuilder withRevocationUri(WorkflowValueResolver<URI> revocationUri) {
this.revocationUri = revocationUri;
return this;
}

HttpRequestInfoBuilder withIntrospectionUri(WorkflowValueResolver<URI> introspectionUri) {
this.introspectionUri = introspectionUri;
return this;
}

HttpRequestInfoBuilder withContentType(OAuth2TokenRequest oAuth2TokenRequest) {
if (oAuth2TokenRequest != null) {
this.contentType = oAuth2TokenRequest.getEncoding().value();
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<String, Object> secret) {
Map<String, Object> 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<String, Object> secret) {
Map<String, Object> client = asClient(secret);
requestBuilder.withGrantType((String) secret.get(GRANT));
addAssertion((String) client.get(ID), (String) client.get(ASSERTION));
}

@Override
protected void password(Map<String, Object> 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<String, Object> asClient(Map<String, Object> secret) {
return (Map<String, Object>) secret.get(CLIENT);
}
Comment on lines +126 to +129
}
Loading
Loading