Skip to content

Commit 4837cd6

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

15 files changed

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

0 commit comments

Comments
 (0)