Skip to content

Commit d0327cb

Browse files
committed
Complete OAuth2 authorization support
Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com>
1 parent cafd804 commit d0327cb

11 files changed

Lines changed: 423 additions & 42 deletions

impl/core/src/main/java/io/serverlessworkflow/impl/auth/AbstractAuthRequestBuilder.java

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,24 @@
1717

1818
import static io.serverlessworkflow.api.types.OAuth2AuthenticationDataClient.ClientAuthentication.CLIENT_SECRET_POST;
1919
import static io.serverlessworkflow.impl.WorkflowUtils.isValid;
20+
import static io.serverlessworkflow.impl.auth.AuthUtils.ACTOR;
21+
import static io.serverlessworkflow.impl.auth.AuthUtils.ACTOR_TOKEN;
22+
import static io.serverlessworkflow.impl.auth.AuthUtils.ACTOR_TOKEN_TYPE;
2023
import static io.serverlessworkflow.impl.auth.AuthUtils.AUDIENCES;
2124
import static io.serverlessworkflow.impl.auth.AuthUtils.AUTHENTICATION;
2225
import static io.serverlessworkflow.impl.auth.AuthUtils.CLIENT;
2326
import static io.serverlessworkflow.impl.auth.AuthUtils.ENCODING;
2427
import static io.serverlessworkflow.impl.auth.AuthUtils.REQUEST;
2528
import static io.serverlessworkflow.impl.auth.AuthUtils.SCOPES;
29+
import static io.serverlessworkflow.impl.auth.AuthUtils.SUBJECT;
30+
import static io.serverlessworkflow.impl.auth.AuthUtils.SUBJECT_TOKEN;
31+
import static io.serverlessworkflow.impl.auth.AuthUtils.SUBJECT_TOKEN_TYPE;
32+
import static io.serverlessworkflow.impl.auth.AuthUtils.TOKEN;
33+
import static io.serverlessworkflow.impl.auth.AuthUtils.TYPE;
2634

2735
import io.serverlessworkflow.api.types.OAuth2AuthenticationData;
2836
import io.serverlessworkflow.api.types.OAuth2AuthenticationDataClient;
37+
import io.serverlessworkflow.api.types.OAuth2TokenDefinition;
2938
import io.serverlessworkflow.impl.WorkflowApplication;
3039
import io.serverlessworkflow.impl.WorkflowUtils;
3140
import java.util.Arrays;
@@ -51,6 +60,7 @@ public HttpRequestInfo apply(T authenticationData) {
5160
audience(authenticationData);
5261
scope(authenticationData);
5362
authenticationMethod(authenticationData);
63+
subjectActor(authenticationData);
5464
return requestBuilder.build();
5565
}
5666

@@ -61,6 +71,7 @@ public HttpRequestInfo apply(Map<String, Object> secret) {
6171
audience(secret);
6272
scope(secret);
6373
authenticationMethod(secret);
74+
subjectActor(secret);
6475
return requestBuilder.build();
6576
}
6677

@@ -80,44 +91,62 @@ protected void audience(Map<String, Object> secret) {
8091
}
8192

8293
protected void authenticationMethod(T authenticationData) {
83-
ClientSecretHandler secretHandler;
84-
switch (getClientAuthentication(authenticationData)) {
85-
case CLIENT_SECRET_BASIC:
86-
secretHandler = new ClientSecretBasic(application, requestBuilder);
87-
case CLIENT_SECRET_JWT:
88-
throw new UnsupportedOperationException("Client Secret JWT is not supported yet");
89-
case PRIVATE_KEY_JWT:
90-
throw new UnsupportedOperationException("Private Key JWT is not supported yet");
91-
default:
92-
secretHandler = new ClientSecretPost(application, requestBuilder);
93-
}
94+
ClientSecretHandler secretHandler =
95+
switch (getClientAuthentication(authenticationData)) {
96+
case CLIENT_SECRET_BASIC -> new ClientSecretBasic(application, requestBuilder);
97+
case CLIENT_SECRET_JWT, PRIVATE_KEY_JWT ->
98+
new JwtClientAssertion(application, requestBuilder);
99+
default -> new ClientSecretPost(application, requestBuilder);
100+
};
94101
secretHandler.accept(authenticationData);
95102
}
96103

104+
@SuppressWarnings("unchecked")
97105
protected void authenticationMethod(Map<String, Object> secret) {
98106
Map<String, Object> client = (Map<String, Object>) secret.get(CLIENT);
99107
ClientSecretHandler secretHandler;
100108
String auth = (String) client.get(AUTHENTICATION);
101109
if (auth == null) {
102110
secretHandler = new ClientSecretPost(application, requestBuilder);
103111
} else {
104-
switch (auth) {
105-
case "client_secret_basic":
106-
secretHandler = new ClientSecretBasic(application, requestBuilder);
107-
break;
108-
default:
109-
case "client_secret_post":
110-
secretHandler = new ClientSecretPost(application, requestBuilder);
111-
break;
112-
case "private_key_jwt":
113-
throw new UnsupportedOperationException("Private Key JWT is not supported yet");
114-
case "client_secret_jwt":
115-
throw new UnsupportedOperationException("Client Secret JWT is not supported yet");
116-
}
112+
secretHandler =
113+
switch (auth) {
114+
case "client_secret_basic" -> new ClientSecretBasic(application, requestBuilder);
115+
case "private_key_jwt", "client_secret_jwt" ->
116+
new JwtClientAssertion(application, requestBuilder);
117+
default -> new ClientSecretPost(application, requestBuilder);
118+
};
117119
}
118120
secretHandler.accept(secret);
119121
}
120122

123+
protected void subjectActor(T authenticationData) {
124+
tokenParam(SUBJECT_TOKEN, SUBJECT_TOKEN_TYPE, authenticationData.getSubject());
125+
tokenParam(ACTOR_TOKEN, ACTOR_TOKEN_TYPE, authenticationData.getActor());
126+
}
127+
128+
private void tokenParam(String tokenKey, String typeKey, OAuth2TokenDefinition definition) {
129+
if (definition != null) {
130+
requestBuilder
131+
.addQueryParam(
132+
tokenKey, WorkflowUtils.buildStringFilter(application, definition.getToken()))
133+
.addQueryParam(typeKey, definition.getType());
134+
}
135+
}
136+
137+
protected void subjectActor(Map<String, Object> secret) {
138+
tokenParam(SUBJECT_TOKEN, SUBJECT_TOKEN_TYPE, secret.get(SUBJECT));
139+
tokenParam(ACTOR_TOKEN, ACTOR_TOKEN_TYPE, secret.get(ACTOR));
140+
}
141+
142+
private void tokenParam(String tokenKey, String typeKey, Object rawDefinition) {
143+
if (rawDefinition instanceof Map<?, ?> definition) {
144+
requestBuilder
145+
.addQueryParam(tokenKey, (String) definition.get(TOKEN))
146+
.addQueryParam(typeKey, (String) definition.get(TYPE));
147+
}
148+
}
149+
121150
private OAuth2AuthenticationDataClient.ClientAuthentication getClientAuthentication(
122151
OAuth2AuthenticationData authenticationData) {
123152
return authenticationData.getClient() == null

impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthUtils.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@ private AuthUtils() {}
3535
public static final String REQUEST = "request";
3636
public static final String ENCODING = "encoding";
3737
public static final String AUTHENTICATION = "authentication";
38+
public static final String ASSERTION = "assertion";
39+
public static final String SUBJECT = "subject";
40+
public static final String ACTOR = "actor";
41+
public static final String TYPE = "type";
42+
public static final String REVOCATION = "revocation";
43+
public static final String INTROSPECTION = "introspection";
44+
45+
public static final String CLIENT_ID = "client_id";
46+
public static final String CLIENT_ASSERTION = "client_assertion";
47+
public static final String CLIENT_ASSERTION_TYPE = "client_assertion_type";
48+
public static final String JWT_BEARER_ASSERTION_TYPE =
49+
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
50+
51+
public static final String SUBJECT_TOKEN = "subject_token";
52+
public static final String SUBJECT_TOKEN_TYPE = "subject_token_type";
53+
public static final String ACTOR_TOKEN = "actor_token";
54+
public static final String ACTOR_TOKEN_TYPE = "actor_token_type";
3855

3956
private static final String AUTH_HEADER_FORMAT = "%s %s";
4057

impl/core/src/main/java/io/serverlessworkflow/impl/auth/ClientSecretHandler.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static io.serverlessworkflow.api.types.OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.CLIENT_CREDENTIALS;
1919
import static io.serverlessworkflow.api.types.OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.PASSWORD;
20+
import static io.serverlessworkflow.api.types.OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.URN_IETF_PARAMS_OAUTH_GRANT_TYPE_TOKEN_EXCHANGE;
2021

2122
import io.serverlessworkflow.api.types.OAuth2AuthenticationData;
2223
import io.serverlessworkflow.impl.WorkflowApplication;
@@ -48,7 +49,8 @@ void accept(OAuth2AuthenticationData authenticationData) {
4849
}
4950

5051
password(authenticationData);
51-
} else if (authenticationData.getGrant().equals(CLIENT_CREDENTIALS)) {
52+
} else if (authenticationData.getGrant().equals(CLIENT_CREDENTIALS)
53+
|| authenticationData.getGrant().equals(URN_IETF_PARAMS_OAUTH_GRANT_TYPE_TOKEN_EXCHANGE)) {
5254
if (authenticationData.getClient() == null
5355
|| authenticationData.getClient().getId() == null
5456
|| authenticationData.getClient().getSecret() == null) {
@@ -74,6 +76,7 @@ void accept(Map<String, Object> secret) {
7476
String grant = Objects.requireNonNull((String) secret.get("grant"), "Grant is mandatory field");
7577
switch (grant) {
7678
case "client_credentials":
79+
case "urn:ietf:params:oauth:grant-type:token-exchange":
7780
clientCredentials(secret);
7881
break;
7982
case "password":

impl/core/src/main/java/io/serverlessworkflow/impl/auth/HttpRequestInfo.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ public record HttpRequestInfo(
2323
Map<String, WorkflowValueResolver<String>> headers,
2424
Map<String, WorkflowValueResolver<String>> queryParams,
2525
WorkflowValueResolver<URI> uri,
26+
WorkflowValueResolver<URI> revocationUri,
27+
WorkflowValueResolver<URI> introspectionUri,
2628
String grantType,
2729
String contentType) {}

impl/core/src/main/java/io/serverlessworkflow/impl/auth/HttpRequestInfoBuilder.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ class HttpRequestInfoBuilder {
3232

3333
private WorkflowValueResolver<URI> uri;
3434

35+
private WorkflowValueResolver<URI> revocationUri;
36+
37+
private WorkflowValueResolver<URI> introspectionUri;
38+
3539
private String grantType;
3640

3741
private String contentType;
@@ -66,6 +70,16 @@ HttpRequestInfoBuilder withUri(WorkflowValueResolver<URI> uri) {
6670
return this;
6771
}
6872

73+
HttpRequestInfoBuilder withRevocationUri(WorkflowValueResolver<URI> revocationUri) {
74+
this.revocationUri = revocationUri;
75+
return this;
76+
}
77+
78+
HttpRequestInfoBuilder withIntrospectionUri(WorkflowValueResolver<URI> introspectionUri) {
79+
this.introspectionUri = introspectionUri;
80+
return this;
81+
}
82+
6983
HttpRequestInfoBuilder withContentType(OAuth2TokenRequest oAuth2TokenRequest) {
7084
if (oAuth2TokenRequest != null) {
7185
this.contentType = oAuth2TokenRequest.getEncoding().value();
@@ -91,6 +105,7 @@ HttpRequestInfo build() {
91105
if (contentType == null) {
92106
contentType = APPLICATION_X_WWW_FORM_URLENCODED.value();
93107
}
94-
return new HttpRequestInfo(headers, queryParams, uri, grantType, contentType);
108+
return new HttpRequestInfo(
109+
headers, queryParams, uri, revocationUri, introspectionUri, grantType, contentType);
95110
}
96111
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl.auth;
17+
18+
import static io.serverlessworkflow.api.types.OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.PASSWORD;
19+
import static io.serverlessworkflow.impl.auth.AuthUtils.ASSERTION;
20+
import static io.serverlessworkflow.impl.auth.AuthUtils.CLIENT;
21+
import static io.serverlessworkflow.impl.auth.AuthUtils.CLIENT_ASSERTION;
22+
import static io.serverlessworkflow.impl.auth.AuthUtils.CLIENT_ASSERTION_TYPE;
23+
import static io.serverlessworkflow.impl.auth.AuthUtils.CLIENT_ID;
24+
import static io.serverlessworkflow.impl.auth.AuthUtils.GRANT;
25+
import static io.serverlessworkflow.impl.auth.AuthUtils.ID;
26+
import static io.serverlessworkflow.impl.auth.AuthUtils.JWT_BEARER_ASSERTION_TYPE;
27+
import static io.serverlessworkflow.impl.auth.AuthUtils.USER;
28+
29+
import io.serverlessworkflow.api.types.OAuth2AuthenticationData;
30+
import io.serverlessworkflow.impl.WorkflowApplication;
31+
import io.serverlessworkflow.impl.WorkflowUtils;
32+
import java.util.Map;
33+
34+
/**
35+
* Handles the {@code client_secret_jwt} and {@code private_key_jwt} client authentication methods.
36+
*
37+
* <p>Per the Serverless Workflow specification, the caller supplies a pre-signed JWT through {@code
38+
* client.assertion}. Both methods are forwarded identically: the assertion is sent as {@code
39+
* client_assertion} together with the standard {@code client_assertion_type} defined by RFC 7523.
40+
* The signing algorithm (HMAC for {@code client_secret_jwt}, an asymmetric key for {@code
41+
* private_key_jwt}) is the caller's responsibility.
42+
*/
43+
class JwtClientAssertion extends ClientSecretHandler {
44+
45+
protected JwtClientAssertion(
46+
WorkflowApplication application, HttpRequestInfoBuilder requestBuilder) {
47+
super(application, requestBuilder);
48+
}
49+
50+
@Override
51+
void accept(OAuth2AuthenticationData authenticationData) {
52+
if (authenticationData.getClient() == null
53+
|| authenticationData.getClient().getAssertion() == null) {
54+
throw new IllegalArgumentException(
55+
"A client assertion must be provided for JWT client authentication");
56+
}
57+
if (authenticationData.getGrant().equals(PASSWORD)) {
58+
if (authenticationData.getUsername() == null || authenticationData.getPassword() == null) {
59+
throw new IllegalArgumentException(
60+
"Username and password must be provided for password grant type");
61+
}
62+
password(authenticationData);
63+
} else {
64+
clientCredentials(authenticationData);
65+
}
66+
}
67+
68+
@Override
69+
void accept(Map<String, Object> secret) {
70+
Map<String, Object> client = asClient(secret);
71+
if (client == null || client.get(ASSERTION) == null) {
72+
throw new IllegalArgumentException(
73+
"A client assertion must be provided for JWT client authentication");
74+
}
75+
if (PASSWORD.value().equals(secret.get(GRANT))) {
76+
password(secret);
77+
} else {
78+
clientCredentials(secret);
79+
}
80+
}
81+
82+
@Override
83+
protected void clientCredentials(OAuth2AuthenticationData authenticationData) {
84+
requestBuilder.withGrantType(authenticationData.getGrant().value());
85+
addAssertion(
86+
authenticationData.getClient().getId(), authenticationData.getClient().getAssertion());
87+
}
88+
89+
@Override
90+
protected void password(OAuth2AuthenticationData authenticationData) {
91+
clientCredentials(authenticationData);
92+
requestBuilder
93+
.addQueryParam(
94+
"username",
95+
WorkflowUtils.buildStringFilter(application, authenticationData.getUsername()))
96+
.addQueryParam(
97+
"password",
98+
WorkflowUtils.buildStringFilter(application, authenticationData.getPassword()));
99+
}
100+
101+
@Override
102+
protected void clientCredentials(Map<String, Object> secret) {
103+
Map<String, Object> client = asClient(secret);
104+
requestBuilder.withGrantType((String) secret.get(GRANT));
105+
addAssertion((String) client.get(ID), (String) client.get(ASSERTION));
106+
}
107+
108+
@Override
109+
protected void password(Map<String, Object> secret) {
110+
clientCredentials(secret);
111+
requestBuilder
112+
.addQueryParam("username", (String) secret.get(USER))
113+
.addQueryParam("password", (String) secret.get(AuthUtils.PASSWORD));
114+
}
115+
116+
private void addAssertion(String clientId, String assertion) {
117+
if (clientId != null) {
118+
requestBuilder.addQueryParam(
119+
CLIENT_ID, WorkflowUtils.buildStringFilter(application, clientId));
120+
}
121+
requestBuilder
122+
.addQueryParam(CLIENT_ASSERTION_TYPE, JWT_BEARER_ASSERTION_TYPE)
123+
.addQueryParam(CLIENT_ASSERTION, WorkflowUtils.buildStringFilter(application, assertion));
124+
}
125+
126+
@SuppressWarnings("unchecked")
127+
private static Map<String, Object> asClient(Map<String, Object> secret) {
128+
return (Map<String, Object>) secret.get(CLIENT);
129+
}
130+
}

0 commit comments

Comments
 (0)