Skip to content

Commit ead2303

Browse files
committed
[Fix #1064] Adding Diggest auth
Signed-off-by: fjtirado <[email protected]>
1 parent 3323167 commit ead2303

15 files changed

+363
-153
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
import io.serverlessworkflow.impl.TaskContext;
1919
import io.serverlessworkflow.impl.WorkflowContext;
2020
import io.serverlessworkflow.impl.WorkflowModel;
21+
import java.net.URI;
2122

2223
public interface AuthProvider {
2324

24-
String authScheme();
25+
String scheme();
2526

26-
String authParameter(WorkflowContext workflow, TaskContext task, WorkflowModel model);
27+
String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri);
2728
}

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,36 +31,40 @@ public static Optional<AuthProvider> getAuth(
3131
WorkflowDefinition definition, EndpointConfiguration configuration) {
3232
return configuration == null
3333
? Optional.empty()
34-
: getAuth(definition, configuration.getAuthentication());
34+
: getAuth(definition, configuration.getAuthentication(), "GET");
3535
}
3636

3737
public static Optional<AuthProvider> getAuth(
38-
WorkflowDefinition definition, ReferenceableAuthenticationPolicy auth) {
38+
WorkflowDefinition definition, ReferenceableAuthenticationPolicy auth, String method) {
3939
if (auth == null) {
4040
return Optional.empty();
4141
}
4242
if (auth.getAuthenticationPolicyReference() != null) {
4343
return buildFromReference(
4444
definition.application(),
4545
definition.workflow(),
46-
auth.getAuthenticationPolicyReference().getUse());
46+
auth.getAuthenticationPolicyReference().getUse(),
47+
method);
4748
} else if (auth.getAuthenticationPolicy() != null) {
4849
return buildFromPolicy(
49-
definition.application(), definition.workflow(), auth.getAuthenticationPolicy());
50+
definition.application(), definition.workflow(), auth.getAuthenticationPolicy(), method);
5051
}
5152
return Optional.empty();
5253
}
5354

5455
private static Optional<AuthProvider> buildFromReference(
55-
WorkflowApplication app, Workflow workflow, String use) {
56+
WorkflowApplication app, Workflow workflow, String use, String method) {
5657
return workflow.getUse().getAuthentications().getAdditionalProperties().entrySet().stream()
5758
.filter(s -> s.getKey().equals(use))
5859
.findAny()
59-
.flatMap(e -> buildFromPolicy(app, workflow, e.getValue()));
60+
.flatMap(e -> buildFromPolicy(app, workflow, e.getValue(), method));
6061
}
6162

6263
private static Optional<AuthProvider> buildFromPolicy(
63-
WorkflowApplication app, Workflow workflow, AuthenticationPolicyUnion authenticationPolicy) {
64+
WorkflowApplication app,
65+
Workflow workflow,
66+
AuthenticationPolicyUnion authenticationPolicy,
67+
String method) {
6468
if (authenticationPolicy.getBasicAuthenticationPolicy() != null) {
6569
return Optional.of(
6670
new BasicAuthProvider(
@@ -70,8 +74,10 @@ private static Optional<AuthProvider> buildFromPolicy(
7074
new BearerAuthProvider(
7175
app, workflow, authenticationPolicy.getBearerAuthenticationPolicy()));
7276
} else if (authenticationPolicy.getDigestAuthenticationPolicy() != null) {
73-
// TODO implement digest authentication
74-
return Optional.empty();
77+
//
78+
return Optional.of(
79+
new DigestAuthProvider(
80+
app, workflow, authenticationPolicy.getDigestAuthenticationPolicy(), method));
7581
} else if (authenticationPolicy.getOAuth2AuthenticationPolicy() != null) {
7682
return Optional.of(
7783
new OAuth2AuthProvider(

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import io.serverlessworkflow.impl.WorkflowModel;
2929
import io.serverlessworkflow.impl.WorkflowUtils;
3030
import io.serverlessworkflow.impl.WorkflowValueResolver;
31+
import java.net.URI;
3132
import java.util.Base64;
3233

3334
class BasicAuthProvider implements AuthProvider {
@@ -57,7 +58,7 @@ public BasicAuthProvider(
5758
}
5859

5960
@Override
60-
public String authParameter(WorkflowContext workflow, TaskContext task, WorkflowModel model) {
61+
public String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) {
6162
return new String(
6263
Base64.getEncoder()
6364
.encode(
@@ -69,7 +70,7 @@ public String authParameter(WorkflowContext workflow, TaskContext task, Workflow
6970
}
7071

7172
@Override
72-
public String authScheme() {
73+
public String scheme() {
7374
return "Basic";
7475
}
7576
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import io.serverlessworkflow.impl.WorkflowModel;
2929
import io.serverlessworkflow.impl.WorkflowUtils;
3030
import io.serverlessworkflow.impl.WorkflowValueResolver;
31+
import java.net.URI;
3132

3233
class BearerAuthProvider implements AuthProvider {
3334

@@ -48,12 +49,12 @@ public BearerAuthProvider(
4849
}
4950

5051
@Override
51-
public String authParameter(WorkflowContext workflow, TaskContext task, WorkflowModel model) {
52+
public String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) {
5253
return tokenFilter.apply(workflow, task, model);
5354
}
5455

5556
@Override
56-
public String authScheme() {
57+
public String scheme() {
5758
return "Bearer";
5859
}
5960
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.serverlessworkflow.impl.WorkflowContext;
2626
import io.serverlessworkflow.impl.WorkflowModel;
2727
import io.serverlessworkflow.impl.WorkflowValueResolver;
28+
import java.net.URI;
2829
import java.util.Arrays;
2930
import java.util.Map;
3031
import java.util.ServiceLoader;
@@ -48,12 +49,12 @@ protected CommonOAuthProvider(WorkflowValueResolver<AccessTokenProvider> tokenPr
4849
}
4950

5051
@Override
51-
public String authParameter(WorkflowContext workflow, TaskContext task, WorkflowModel model) {
52+
public String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) {
5253
return tokenProvider.apply(workflow, task, model).validateAndGet(workflow, task, model).token();
5354
}
5455

5556
@Override
56-
public String authScheme() {
57+
public String scheme() {
5758
return "Bearer";
5859
}
5960

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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.impl.WorkflowUtils.checkSecret;
19+
import static io.serverlessworkflow.impl.WorkflowUtils.secretProp;
20+
import static io.serverlessworkflow.impl.auth.AuthUtils.PASSWORD;
21+
import static io.serverlessworkflow.impl.auth.AuthUtils.USER;
22+
23+
import io.serverlessworkflow.api.types.DigestAuthenticationPolicy;
24+
import io.serverlessworkflow.api.types.DigestAuthenticationProperties;
25+
import io.serverlessworkflow.api.types.Workflow;
26+
import io.serverlessworkflow.impl.TaskContext;
27+
import io.serverlessworkflow.impl.WorkflowApplication;
28+
import io.serverlessworkflow.impl.WorkflowContext;
29+
import io.serverlessworkflow.impl.WorkflowModel;
30+
import io.serverlessworkflow.impl.WorkflowUtils;
31+
import io.serverlessworkflow.impl.WorkflowValueResolver;
32+
import java.io.IOException;
33+
import java.io.UncheckedIOException;
34+
import java.net.HttpURLConnection;
35+
import java.net.URI;
36+
import java.security.MessageDigest;
37+
import java.security.NoSuchAlgorithmException;
38+
import java.util.Arrays;
39+
import java.util.Collection;
40+
import java.util.EnumSet;
41+
import java.util.Optional;
42+
import java.util.Set;
43+
import java.util.StringTokenizer;
44+
import java.util.concurrent.atomic.AtomicInteger;
45+
import java.util.stream.Collectors;
46+
47+
class DigestAuthProvider implements AuthProvider {
48+
49+
private static final String NONCE = "nonce";
50+
private static final String REALM = "realm";
51+
private static final String QOP_KEY = "qop";
52+
private static final String OPAQUE = "opaque";
53+
54+
private static class DigestServerInfo {
55+
56+
private Algorithm algorithm = Algorithm.MD5;
57+
private String nonce;
58+
private String opaque;
59+
private String realm;
60+
private Collection<QOP> qop = Set.of();
61+
62+
public static DigestServerInfo from(String header) {
63+
DigestServerInfo serverInfo = new DigestServerInfo();
64+
StringTokenizer tokenizer = new StringTokenizer(header);
65+
while (tokenizer.hasMoreElements()) {
66+
String token = tokenizer.nextToken();
67+
68+
int indexOf = token.indexOf("=");
69+
if (indexOf != -1) {
70+
String key = token.substring(0, indexOf).trim().toLowerCase();
71+
String value = token.substring(indexOf + 1).trim();
72+
switch (key) {
73+
case "algorithm":
74+
serverInfo.algorithm = Algorithm.valueOf(value.toUpperCase());
75+
break;
76+
case NONCE:
77+
serverInfo.nonce = value;
78+
break;
79+
case OPAQUE:
80+
serverInfo.opaque = value;
81+
break;
82+
case REALM:
83+
serverInfo.realm = value;
84+
break;
85+
case QOP_KEY:
86+
serverInfo.qop =
87+
Arrays.stream(value.split(","))
88+
.map(String::toUpperCase)
89+
.map(QOP::valueOf)
90+
.collect(Collectors.toCollection(() -> EnumSet.noneOf(QOP.class)));
91+
break;
92+
}
93+
}
94+
}
95+
return serverInfo;
96+
}
97+
98+
boolean isAuthQOP() {
99+
return qop.contains(QOP.AUTH) || qop.contains(QOP.AUTH_INT);
100+
}
101+
102+
Optional<String> qop() {
103+
return qop.isEmpty()
104+
? Optional.empty()
105+
: Optional.of(qop.iterator().next().toString().toLowerCase());
106+
}
107+
}
108+
109+
private static enum Algorithm {
110+
MD5,
111+
MD5SESSS
112+
};
113+
114+
private static enum QOP {
115+
AUTH,
116+
AUTH_INT,
117+
};
118+
119+
private final WorkflowValueResolver<String> userFilter;
120+
private final WorkflowValueResolver<String> passwordFilter;
121+
private final String method;
122+
123+
public DigestAuthProvider(
124+
WorkflowApplication app,
125+
Workflow workflow,
126+
DigestAuthenticationPolicy authPolicy,
127+
String method) {
128+
DigestAuthenticationProperties properties =
129+
authPolicy.getDigest().getDigestAuthenticationProperties();
130+
if (properties != null) {
131+
userFilter = WorkflowUtils.buildStringFilter(app, properties.getUsername());
132+
passwordFilter = WorkflowUtils.buildStringFilter(app, properties.getPassword());
133+
} else if (authPolicy.getDigest().getDigestAuthenticationPolicySecret() != null) {
134+
String secretName =
135+
checkSecret(workflow, authPolicy.getDigest().getDigestAuthenticationPolicySecret());
136+
userFilter = (w, t, m) -> secretProp(w, secretName, USER);
137+
passwordFilter = (w, t, m) -> secretProp(w, secretName, PASSWORD);
138+
} else {
139+
throw new IllegalStateException(
140+
"Both secret and properties are null for digest authorization");
141+
}
142+
this.method = method;
143+
}
144+
145+
@Override
146+
public String scheme() {
147+
return "Digest";
148+
}
149+
150+
@Override
151+
public String content(WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) {
152+
try {
153+
HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
154+
connection.setRequestMethod(method);
155+
int responseCode = connection.getResponseCode();
156+
if (responseCode == 401) {
157+
DigestServerInfo serverInfo =
158+
DigestServerInfo.from(connection.getHeaderField("WWW-Authenticate"));
159+
String userName = userFilter.apply(workflow, task, model);
160+
String path = uri.getPath();
161+
String ha1 =
162+
calculateHash(userName, serverInfo.realm, passwordFilter.apply(workflow, task, model));
163+
164+
String nonceCount;
165+
String clientNonce;
166+
if (serverInfo.isAuthQOP() || serverInfo.algorithm == Algorithm.MD5SESSS) {
167+
nonceCount = Integer.toString(nc.getAndIncrement());
168+
clientNonce = getClientNonce(nonceCount);
169+
} else {
170+
nonceCount = null;
171+
clientNonce = null;
172+
}
173+
String response;
174+
if (serverInfo.algorithm == Algorithm.MD5SESSS) {
175+
ha1 = calculateHash(ha1, serverInfo.nonce, clientNonce);
176+
}
177+
String ha2 = calculateHash(String.format("%s:%s", method, uri));
178+
if (serverInfo.isAuthQOP()) {
179+
response =
180+
calculateHash(
181+
ha1,
182+
serverInfo.nonce,
183+
nonceCount,
184+
clientNonce,
185+
serverInfo.qop().orElseThrow(),
186+
ha2);
187+
} else {
188+
response = calculateHash(ha1, serverInfo.nonce, ha2);
189+
}
190+
191+
return buildResponseInfo(serverInfo, userName, path, clientNonce, nonceCount, response);
192+
} else {
193+
throw new IllegalStateException(
194+
"URI "
195+
+ uri
196+
+ " is not digest protected, it returned code "
197+
+ responseCode
198+
+ " when invoked without authentication header, but it should have returned 401 as per RFC 2617");
199+
}
200+
} catch (IOException io) {
201+
throw new UncheckedIOException(io);
202+
}
203+
}
204+
205+
private String buildResponseInfo(
206+
DigestServerInfo digestInfo,
207+
String userName,
208+
String uri,
209+
String clientNonce,
210+
String nonceCount,
211+
String response) {
212+
StringBuilder sb = new StringBuilder("username=" + userName);
213+
addHeader(sb, "uri", uri);
214+
addHeader(sb, "response", response);
215+
addHeader(sb, NONCE, digestInfo.nonce);
216+
addHeader(sb, REALM, digestInfo.realm);
217+
if (digestInfo.opaque != null) {
218+
addHeader(sb, OPAQUE, digestInfo.opaque);
219+
}
220+
digestInfo.qop().ifPresent(qop -> addHeader(sb, QOP_KEY, qop));
221+
222+
if (clientNonce != null) {
223+
addHeader(sb, "cnonce", clientNonce);
224+
addHeader(sb, "nc", nonceCount);
225+
}
226+
return sb.toString();
227+
}
228+
229+
private StringBuilder addHeader(StringBuilder sb, String key, String value) {
230+
return sb.append(',').append(key).append('=').append(value);
231+
}
232+
233+
private static AtomicInteger nc = new AtomicInteger(1);
234+
235+
private static String getClientNonce(String nonceCount) {
236+
return "impl-" + nonceCount;
237+
}
238+
239+
private String calculateHash(String firstOne, String... strs) {
240+
try {
241+
242+
MessageDigest md = MessageDigest.getInstance("MD5");
243+
StringBuilder sb = new StringBuilder(firstOne);
244+
for (String str : strs) {
245+
sb.append(':').append(str);
246+
}
247+
return new String(md.digest(sb.toString().getBytes()));
248+
} catch (NoSuchAlgorithmException ex) {
249+
throw new UnsupportedOperationException("System is not supporting MD5!!!!", ex);
250+
}
251+
}
252+
}

0 commit comments

Comments
 (0)