调用DUO Security Admin API /admin/v1/users返回40103错误求助
调用Duo Security /admin/v1/users接口时40103签名错误排查
错误响应
{ "code":40103, "message":"Invalid signature in request credentials", "stat":"FAIL" }
问题代码
public class DuoCreateUsers { private static SortedMap<String, Object> params = new TreeMap<String, Object>(); public static void main(String[] args) throws IOException { String ikey = "x"; String skey = "x"; String host = "api-d221a358.duosecurity.com"; String httpMethod = "POST"; String requestPath = "/admin/v1/users"; String timestamp = OffsetDateTime.now().format(DateTimeFormatter.RFC_1123_DATE_TIME); String username = "newuser"; String email = "abc@example.com"; params.put("username",username); params.put("email",email); String queryString = canonQueryString(); String canonicalRequest = timestamp + "\n" + httpMethod + "\n" + requestPath + "\n" + queryString; System.out.println("canonicalRequest = " + canonicalRequest); String signature = sign1(canonicalRequest, skey); // System.out.println("signature = " + signature); String url = "https://" + host + requestPath+"?"+queryString; System.out.println("url = " + url); HttpClient httpClient = HttpClients.createDefault(); HttpPost httpPost = new HttpPost(url); httpPost.setHeader("Date", timestamp); httpPost.setHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString((ikey + ":" + signature).getBytes())); httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded"); httpPost.setEntity(new StringEntity(params.toString())); // Make the request HttpResponse response = httpClient.execute(httpPost); HttpEntity entity = response.getEntity(); String responseContent = entity != null ? EntityUtils.toString(entity) : ""; String rs = "Response Status Code: " + response.getStatusLine().getStatusCode() + "\nResponse Content:\n" + responseContent; System.out.println("rs = " + rs); } public static String canonQueryString() throws UnsupportedEncodingException { ArrayList<String> args = new ArrayList<String>(); for (String key : params.keySet()) { String name = URLEncoder .encode(key, "UTF-8") .replace("+", "%20") .replace("*", "%2A") .replace("%7E", "~"); String value = URLEncoder .encode(params.get(key).toString(), "UTF-8") .replace("+", "%20") .replace("*", "%2A") .replace("%7E", "~"); args.add(name + "=" + value); } return com.duosecurity.client.Util.join(args.toArray(), "&"); } private static String sign1(String data, String secretKey) { try { // Create an HMAC-SHA1 key from the secret key SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA1"); // Initialize the HMAC-SHA1 algorithm Mac mac = Mac.getInstance("HmacSHA1"); mac.init(secretKeySpec); // Calculate the HMAC-SHA1 hash byte[] hmacSha1Bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); // Convert the result to a hexadecimal string String hmacSha1Hex = bytesToHex(hmacSha1Bytes); // Print the HMAC-SHA1 hash System.out.println("HMAC-SHA1: " + hmacSha1Hex); return hmacSha1Hex; } catch (NoSuchAlgorithmException | InvalidKeyException e) { e.printStackTrace(); } return null; } // Helper method to convert bytes to a hexadecimal string private static String bytesToHex(byte[] bytes) { StringBuilder hexStringBuilder = new StringBuilder(); for (byte b : bytes) { hexStringBuilder.append(String.format("%02x", b)); } return hexStringBuilder.toString(); } }
代码问题排查
- Timestamp格式错误:Duo API要求签名用的timestamp是Unix时间戳(以秒为单位的十进制字符串),你当前使用RFC1123格式的日期字符串,导致签名计算的时间基准与服务器验证逻辑不匹配,直接触发签名无效。
- Authorization头格式错误:Duo API的认证头格式为
DUO <integration_key>:<signature>,你误用了HTTP Basic认证格式(Basic ...),服务器无法正确解析签名信息。 - POST参数位置错误:/admin/v1/users是POST接口,参数需通过请求体以
application/x-www-form-urlencoded形式发送,你同时将参数放到URL query和请求体中,导致签名计算的内容与实际请求参数不一致。 - 签名计算遗漏请求体:POST请求的canonical request需包含URL编码后的请求体参数,你当前用URL query的内容参与签名,但实际参数在请求体,二者不匹配导致签名验证失败。
- 请求体格式错误:
params.toString()生成的是Map的字符串表示,并非符合要求的application/x-www-form-urlencoded格式键值对,服务器无法正确解析参数。
修正后的关键代码片段
public static void main(String[] args) throws IOException { String ikey = "x"; String skey = "x"; String host = "api-d221a358.duosecurity.com"; String httpMethod = "POST"; String requestPath = "/admin/v1/users"; // 修正为Unix时间戳(秒) String timestamp = String.valueOf(System.currentTimeMillis() / 1000); String username = "newuser"; String email = "abc@example.com"; params.put("username",username); params.put("email",email); // POST请求无URL query,设为空字符串 String queryString = ""; // 生成符合要求的表单请求体内容 String formBody = canonQueryString(); // 修正canonicalRequest:使用请求体内容参与签名 String canonicalRequest = timestamp + "\n" + httpMethod + "\n" + requestPath + "\n" + formBody; System.out.println("canonicalRequest = " + canonicalRequest); String signature = sign1(canonicalRequest, skey); // URL无需拼接query参数 String url = "https://" + host + requestPath; System.out.println("url = " + url); HttpClient httpClient = HttpClients.createDefault(); HttpPost httpPost = new HttpPost(url); httpPost.setHeader("Date", timestamp); // 修正Authorization头格式 httpPost.setHeader("Authorization", "DUO " + ikey + ":" + signature); httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded"); // 使用正确的表单内容作为请求体 httpPost.setEntity(new StringEntity(formBody, StandardCharsets.UTF_8)); // 后续请求执行代码不变 HttpResponse response = httpClient.execute(httpPost); HttpEntity entity = response.getEntity(); String responseContent = entity != null ? EntityUtils.toString(entity) : ""; String rs = "Response Status Code: " + response.getStatusLine().getStatusCode() + "\nResponse Content:\n" + responseContent; System.out.println("rs = " + rs); }
内容的提问来源于stack exchange,提问作者Enamul Haque
相关产品推荐
相关产品推荐

