|
| 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