Skip to content

Commit 8ba3d56

Browse files
committed
[Fix #1064] Adding Diggest auth
Signed-off-by: fjtirado <ftirados@redhat.com>
1 parent 3323167 commit 8ba3d56

15 files changed

+338
-144
lines changed

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,20 @@
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+
/**
28+
* @param workflow
29+
* @param task
30+
* @param model
31+
* @param uri the protected URI (which might be needed for some authentication modes, like
32+
* digest), do not mistake with token URI
33+
* @return
34+
*/
35+
String content(
36+
WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri, String method);
2737
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@ private static Optional<AuthProvider> buildFromPolicy(
7070
new BearerAuthProvider(
7171
app, workflow, authenticationPolicy.getBearerAuthenticationPolicy()));
7272
} else if (authenticationPolicy.getDigestAuthenticationPolicy() != null) {
73-
// TODO implement digest authentication
74-
return Optional.empty();
73+
//
74+
return Optional.of(
75+
new DigestAuthProvider(
76+
app, workflow, authenticationPolicy.getDigestAuthenticationPolicy()));
7577
} else if (authenticationPolicy.getOAuth2AuthenticationPolicy() != null) {
7678
return Optional.of(
7779
new OAuth2AuthProvider(

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

Lines changed: 4 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,8 @@ public BasicAuthProvider(
5758
}
5859

5960
@Override
60-
public String authParameter(WorkflowContext workflow, TaskContext task, WorkflowModel model) {
61+
public String content(
62+
WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri, String method) {
6163
return new String(
6264
Base64.getEncoder()
6365
.encode(
@@ -69,7 +71,7 @@ public String authParameter(WorkflowContext workflow, TaskContext task, Workflow
6971
}
7072

7173
@Override
72-
public String authScheme() {
74+
public String scheme() {
7375
return "Basic";
7476
}
7577
}

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

Lines changed: 4 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,13 @@ public BearerAuthProvider(
4849
}
4950

5051
@Override
51-
public String authParameter(WorkflowContext workflow, TaskContext task, WorkflowModel model) {
52+
public String content(
53+
WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri, String method) {
5254
return tokenFilter.apply(workflow, task, model);
5355
}
5456

5557
@Override
56-
public String authScheme() {
58+
public String scheme() {
5759
return "Bearer";
5860
}
5961
}

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

Lines changed: 4 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,13 @@ protected CommonOAuthProvider(WorkflowValueResolver<AccessTokenProvider> tokenPr
4849
}
4950

5051
@Override
51-
public String authParameter(WorkflowContext workflow, TaskContext task, WorkflowModel model) {
52+
public String content(
53+
WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri, String method) {
5254
return tokenProvider.apply(workflow, task, model).validateAndGet(workflow, task, model).token();
5355
}
5456

5557
@Override
56-
public String authScheme() {
58+
public String scheme() {
5759
return "Bearer";
5860
}
5961

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

impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,22 @@ public <T> T load(
9999
WorkflowContext workflowContext,
100100
TaskContext taskContext,
101101
WorkflowModel model) {
102-
return loadURI(
102+
URI uri =
103103
uriSupplier(endPoint)
104104
.apply(
105105
workflowContext,
106106
taskContext,
107-
model == null ? application.modelFactory().fromNull() : model),
107+
model == null ? application.modelFactory().fromNull() : model);
108+
return loadURI(
109+
uri,
108110
function,
109111
AuthProviderFactory.getAuth(
110112
workflowContext.definition(), endPoint.getEndpointConfiguration())
111113
.map(
112114
auth ->
113115
AuthUtils.authHeaderValue(
114-
auth.authScheme(),
115-
auth.authParameter(workflowContext, taskContext, model))));
116+
auth.scheme(),
117+
auth.content(workflowContext, taskContext, model, uri, "GET"))));
116118
}
117119

118120
public <T> T loadURI(URI uri, Function<ExternalResourceHandler, T> function) {

0 commit comments

Comments
 (0)