|
| 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 | +} |
0 commit comments